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

Add dummy --update-docblocks option

This commit is contained in:
Matthew Brown 2016-11-12 18:51:48 -05:00
parent 8c64dfd7fb
commit 8dfca6cce2
11 changed files with 277 additions and 99 deletions

View File

@ -19,7 +19,7 @@ ini_set('memory_limit', '2048M');
ini_set('xdebug.max_nesting_level', 512);
// get options from command line
$options = getopt('f:m:hc:', ['help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff', 'file:', 'self-check']);
$options = getopt('f:m:hc:', ['help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff', 'file:', 'self-check', 'update-docblocks']);
if (array_key_exists('help', $options)) {
$options['h'] = false;
@ -112,6 +112,8 @@ $show_info = isset($options['show-info'])
$is_diff = isset($options['diff']);
$update_docblocks = isset($options['update-docblocks']);
// initialise custom config, if passed
if ($path_to_config) {
ProjectChecker::setConfigXML($path_to_config);
@ -129,9 +131,9 @@ if (array_key_exists('self-check', $options)) {
} elseif ($paths_to_check) {
foreach ($paths_to_check as $path_to_check) {
if (is_dir($path_to_check)) {
ProjectChecker::checkDir($path_to_check, $debug);
ProjectChecker::checkDir($path_to_check, $debug, $update_docblocks);
} else {
ProjectChecker::checkFile($path_to_check, $debug);
ProjectChecker::checkFile($path_to_check, $debug, $update_docblocks);
}
}
}

View File

@ -202,6 +202,7 @@ abstract class ClassLikeChecker implements StatementsSource
public function __construct(PhpParser\Node\Stmt\ClassLike $class, StatementsSource $source, $fq_class_name)
{
$this->class = $class;
$this->source = $source;
$this->namespace = $source->getNamespace();
$this->aliased_classes = $source->getAliasedClasses();
$this->file_name = $source->getFileName();
@ -223,7 +224,7 @@ abstract class ClassLikeChecker implements StatementsSource
* @param Context|null $class_context
* @return false|null
*/
public function check($check_methods = true, Context $class_context = null)
public function check($check_methods = true, Context $class_context = null, $update_docblocks = false)
{
if (!$check_methods &&
!($this instanceof TraitChecker) &&
@ -403,7 +404,7 @@ abstract class ClassLikeChecker implements StatementsSource
$method_checker->check(clone $class_context);
if (!$config->excludeIssueInFile('InvalidReturnType', $this->file_name)) {
$method_checker->checkReturnTypes();
$method_checker->checkReturnTypes($update_docblocks);
}
}
}
@ -852,6 +853,14 @@ abstract class ClassLikeChecker implements StatementsSource
return $this->aliased_classes;
}
/**
* @return array<string, string>
*/
public function getAliasedClassesFlipped()
{
return $this->source->getAliasedClassesFlipped();
}
/**
* @return string
*/

View File

@ -29,15 +29,25 @@ class FileChecker implements StatementsSource
protected $include_file_name;
/**
* @var array
* @var array<string, string>
*/
protected $aliased_classes = [];
/**
* @var array<string, array<int, string>>
* @var array<string, string>
*/
protected $aliased_classes_flipped = [];
/**
* @var array<string, array<string, string>>
*/
protected $namespace_aliased_classes = [];
/**
* @var array<string, array<string, string>>
*/
protected $namespace_aliased_classes_flipped = [];
/**
* @var array<int, \PhpParser\Node>
*/
@ -116,6 +126,13 @@ class FileChecker implements StatementsSource
*/
protected static $deleted_files = null;
/**
* A list of return types, keyed by file
*
* @var array<string, array<int, string>>
*/
protected static $docblock_return_types = [];
/**
* @param string $file_name
* @param array $preloaded_statements
@ -140,8 +157,13 @@ class FileChecker implements StatementsSource
* @param bool $cache
* @return array|null
*/
public function check($check_classes = true, $check_functions = true, Context $file_context = null, $cache = true)
{
public function check(
$check_classes = true,
$check_functions = true,
Context $file_context = null,
$cache = true,
$update_docblocks = false
) {
if ($cache && isset(self::$functions_checked[$this->short_file_name])) {
return null;
}
@ -195,7 +217,7 @@ class FileChecker implements StatementsSource
?: new ClassChecker($stmt, $this, $stmt->name);
$this->declared_classes[] = $class_checker->getFQCLN();
$class_checker->check($check_functions);
$class_checker->check($check_functions, null, $update_docblocks);
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Interface_) {
if ($check_classes) {
@ -218,11 +240,16 @@ class FileChecker implements StatementsSource
$namespace_name = implode('\\', $stmt->name->parts);
$namespace_checker = new NamespaceChecker($stmt, $this);
$this->namespace_aliased_classes[$namespace_name] = $namespace_checker->check(
$namespace_checker->check(
$check_classes,
$check_functions
$check_functions,
$update_docblocks
);
$this->namespace_aliased_classes[$namespace_name] = $namespace_checker->getAliasedClasses();
$this->namespace_aliased_classes_flipped[$namespace_name] = $namespace_checker->getAliasedClassesFlipped();
$this->declared_classes = array_merge($namespace_checker->getDeclaredClasses());
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_ && $check_functions) {
$function_context = new Context($this->short_file_name, $file_context->self);
@ -426,7 +453,7 @@ class FileChecker implements StatementsSource
/**
* @param string|null $namespace_name
* @return array<string>
* @return array<string, string>
*/
public function getAliasedClasses($namespace_name = null)
{
@ -437,6 +464,19 @@ class FileChecker implements StatementsSource
return $this->aliased_classes;
}
/**
* @param string|null $namespace_name
* @return array<string, string>
*/
public function getAliasedClassesFlipped($namespace_name = null)
{
if ($namespace_name && isset($this->namespace_aliased_classes_flipped[$namespace_name])) {
return $this->namespace_aliased_classes_flipped[$namespace_name];
}
return $this->aliased_classes_flipped;
}
/**
* @return null
*/
@ -576,6 +616,7 @@ class FileChecker implements StatementsSource
if ($stmt instanceof PhpParser\Node\Stmt\Use_) {
foreach ($stmt->uses as $use) {
$this->aliased_classes[strtolower($use->alias)] = implode('\\', $use->name->parts);
$this->aliased_classes_flipped[implode('\\', $use->name->parts)] = $use->alias;
}
}
}
@ -842,4 +883,22 @@ class FileChecker implements StatementsSource
{
return md5($file_name);
}
/**
* Adds a docblock to the given file
* @param string $file_name
* @param int $line_number
* @param string $new_type
*/
public static function addDocblockReturnType($file_name, $line_number, $new_type)
{
if ($new_type === 'null') {
$new_type = 'void';
}
$new_type = str_replace('<mixed, mixed>', '', $new_type);
var_dump($file_name . ':' . $line_number . ' - ' . $new_type);
self::$docblock_return_types[$file_name][$line_number] = $new_type;
}
}

View File

@ -343,13 +343,21 @@ abstract class FunctionLikeChecker implements StatementsSource
}
/**
* @return array<string>
* @return array<string, string>
*/
public function getAliasedClasses()
{
return $this->source->getAliasedClasses();
}
/**
* @return array<string, string>
*/
public function getAliasedClassesFlipped()
{
return $this->source->getAliasedClassesFlipped();
}
/**
* @return string
*/
@ -442,10 +450,10 @@ abstract class FunctionLikeChecker implements StatementsSource
}
/**
* @param bool $update_doc_comment
* @param bool $update_docblock
* @return false|null
*/
public function checkReturnTypes($update_doc_comment = false)
public function checkReturnTypes($update_docblock = false)
{
if (!$this->function->getStmts()) {
return null;
@ -469,7 +477,7 @@ abstract class FunctionLikeChecker implements StatementsSource
}
}
if (!$method_return_types) {
if (!$method_return_types && !$update_docblock) {
if (IssueBuffer::accepts(
new MissingReturnType(
'Method ' . $cased_method_id . ' does not have a return type',
@ -484,6 +492,35 @@ abstract class FunctionLikeChecker implements StatementsSource
return null;
}
$inferred_yield_types = [];
$inferred_return_types = EffectsAnalyser::getReturnTypes(
$this->function->getStmts(),
$inferred_yield_types,
true
);
$inferred_return_type = $inferred_return_types ? Type::combineTypes($inferred_return_types) : null;
$inferred_yield_type = $inferred_yield_types ? Type::combineTypes($inferred_yield_types) : null;
$inferred_generator_return_type = null;
if ($inferred_yield_type) {
$inferred_generator_return_type = $inferred_return_type;
$inferred_return_type = $inferred_yield_type;
}
if (!$method_return_types && $update_docblock) {
if ($inferred_return_type) {
FileChecker::addDocblockReturnType(
$this->file_name,
$this->function->getLine(),
$inferred_return_type->toNamespacedString($this->getAliasedClassesFlipped(), $this->getFQCLN())
);
}
return null;
}
// passing it through fleshOutTypes eradicates errant $ vars
$declared_return_type = ExpressionChecker::fleshOutTypes(
$method_return_types,
@ -492,32 +529,45 @@ abstract class FunctionLikeChecker implements StatementsSource
$method_id
);
if ($declared_return_type) {
$inferred_yield_types = [];
$inferred_return_types = EffectsAnalyser::getReturnTypes(
$this->function->getStmts(),
$inferred_yield_types,
true
);
if (!$inferred_return_type && !$inferred_yield_types) {
if ($declared_return_type->isVoid()) {
return null;
}
if (!$inferred_return_types && !$inferred_yield_types) {
if ($declared_return_type->isVoid()) {
return null;
}
if (ScopeChecker::onlyThrows($this->function->getStmts())) {
// if there's a single throw statement, it's presumably an exception saying this method is not to be
// used
return null;
}
if (ScopeChecker::onlyThrows($this->function->getStmts())) {
// if there's a single throw statement, it's presumably an exception saying this method is not to be
// used
return null;
}
if (IssueBuffer::accepts(
new InvalidReturnType(
'No return type was found for method ' . $cased_method_id .
' but return type \'' . $declared_return_type . '\' was expected',
$this->getCheckedFileName(),
$this->function->getLine()
)
)) {
return false;
}
return null;
}
if ($inferred_return_type && !$declared_return_type->isMixed()) {
if ($inferred_return_type->isNull() && $declared_return_type->isVoid()) {
return null;
}
if ($inferred_return_type->isMixed()) {
if (IssueBuffer::accepts(
new InvalidReturnType(
'No return type was found for method ' . $cased_method_id .
' but return type \'' . $declared_return_type . '\' was expected',
new MixedInferredReturnType(
'Could not verify return type \'' . $declared_return_type . '\' for ' .
$cased_method_id,
$this->getCheckedFileName(),
$this->function->getLine()
)
),
$this->getSuppressedIssues()
)) {
return false;
}
@ -525,58 +575,35 @@ abstract class FunctionLikeChecker implements StatementsSource
return null;
}
$inferred_return_type = $inferred_return_types ? Type::combineTypes($inferred_return_types) : null;
if ($update_docblock) {
if ((string)$inferred_return_type !== (string)$declared_return_type) {
FileChecker::addDocblockReturnType(
$this->file_name,
$this->function->getLine(),
$inferred_return_type->toNamespacedString($this->getAliasedClassesFlipped(), $this->getFQCLN())
);
}
$inferred_yield_type = $inferred_yield_types ? Type::combineTypes($inferred_yield_types) : null;
$inferred_generator_return_type = null;
if ($inferred_yield_type) {
$inferred_generator_return_type = $inferred_return_type;
$inferred_return_type = $inferred_yield_type;
return null;
}
if ($inferred_return_type && !$declared_return_type->isMixed()) {
if ($inferred_return_type->isNull() && $declared_return_type->isVoid()) {
return null;
}
if ($inferred_return_type->isMixed()) {
if (IssueBuffer::accepts(
new MixedInferredReturnType(
'Could not verify return type \'' . $declared_return_type . '\' for ' .
$cased_method_id,
$this->getCheckedFileName(),
$this->function->getLine()
),
$this->getSuppressedIssues()
)) {
return false;
}
return null;
}
if (!TypeChecker::hasIdenticalTypes(
$declared_return_type,
$inferred_return_type,
$this->fq_class_name
if (!TypeChecker::hasIdenticalTypes(
$declared_return_type,
$inferred_return_type,
$this->fq_class_name
)) {
if (IssueBuffer::accepts(
new InvalidReturnType(
'The given return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
' is incorrect, got \'' . $inferred_return_type . '\'',
$this->getCheckedFileName(),
$this->function->getLine()
),
$this->getSuppressedIssues()
)) {
if (IssueBuffer::accepts(
new InvalidReturnType(
'The given return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
' is incorrect, got \'' . $inferred_return_type . '\'',
$this->getCheckedFileName(),
$this->function->getLine()
),
$this->getSuppressedIssues()
)) {
return false;
}
return false;
}
}
return null;
}
return null;

View File

@ -24,10 +24,15 @@ class NamespaceChecker implements StatementsSource
protected $declared_classes = [];
/**
* @var array
* @var array<string, string>
*/
protected $aliased_classes = [];
/**
* @var array<string, string>
*/
protected $aliased_classes_flipped = [];
/**
* @var string
*/
@ -59,9 +64,9 @@ class NamespaceChecker implements StatementsSource
/**
* @param bool $check_classes
* @param bool $check_class_statements
* @return array
* @return void
*/
public function check($check_classes = true, $check_class_statements = true)
public function check($check_classes = true, $check_class_statements = true, $update_docblocks = false)
{
$leftover_stmts = [];
@ -76,7 +81,7 @@ class NamespaceChecker implements StatementsSource
$class_checker = ClassLikeChecker::getClassLikeCheckerFromClass($fq_class_name)
?: new ClassChecker($stmt, $this, $fq_class_name);
$class_checker->check($check_class_statements);
$class_checker->check($check_class_statements, null, $update_docblocks);
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Interface_) {
if ($check_classes) {
@ -95,6 +100,7 @@ class NamespaceChecker implements StatementsSource
} elseif ($stmt instanceof PhpParser\Node\Stmt\Use_) {
foreach ($stmt->uses as $use) {
$this->aliased_classes[strtolower($use->alias)] = implode('\\', $use->name->parts);
$this->aliased_classes_flipped[implode('\\', $use->name->parts)] = strtolower($use->alias);
}
} else {
$leftover_stmts[] = $stmt;
@ -106,8 +112,6 @@ class NamespaceChecker implements StatementsSource
$context = new Context($this->file_name);
$statments_checker->check($leftover_stmts, $context);
}
return $this->aliased_classes;
}
/**
@ -145,6 +149,14 @@ class NamespaceChecker implements StatementsSource
return $this->aliased_classes;
}
/**
* @return array
*/
public function getAliasedClassesFlipped()
{
return $this->aliased_classes_flipped;
}
/**
* @return null
*/

View File

@ -35,7 +35,7 @@ class ProjectChecker
* @param boolean $is_diff
* @return void
*/
public static function check($debug = false, $is_diff = false)
public static function check($debug = false, $is_diff = false, $update_docblocks = false)
{
$cwd = getcwd();
@ -65,7 +65,7 @@ class ProjectChecker
if ($diff_files === null || $deleted_files === null || count($diff_files) > 200) {
foreach (self::$config->getIncludeDirs() as $dir_name) {
self::checkDirWithConfig($dir_name, self::$config, $debug);
self::checkDirWithConfig($dir_name, self::$config, $debug, $update_docblocks);
}
} else {
if ($debug) {
@ -121,7 +121,7 @@ class ProjectChecker
* @param bool $debug
* @return void
*/
protected static function checkDirWithConfig($dir_name, Config $config, $debug)
protected static function checkDirWithConfig($dir_name, Config $config, $debug, $update_docblocks)
{
$file_extensions = $config->getFileExtensions();
$filetype_handlers = $config->getFiletypeHandlers();
@ -147,7 +147,7 @@ class ProjectChecker
$file_checker = new FileChecker($file_name);
}
$file_checker->check(true);
$file_checker->check(true, true, null, true, $update_docblocks);
}
}
@ -266,7 +266,7 @@ class ProjectChecker
* @param boolean $debug
* @return void
*/
public static function checkFile($file_name, $debug = false)
public static function checkFile($file_name, $debug = false, $update_docblocks = false)
{
if ($debug) {
echo 'Checking ' . $file_name . PHP_EOL;
@ -295,7 +295,7 @@ class ProjectChecker
$file_checker = new FileChecker($file_name);
}
$file_checker->check(true);
$file_checker->check(true, true, null, true, $update_docblocks);
IssueBuffer::finish();
}

View File

@ -41,7 +41,7 @@ class TraitChecker extends ClassLikeChecker
* @param Context|null $class_context
* @return void
*/
public function check($check_methods = true, Context $class_context = null)
public function check($check_methods = true, Context $class_context = null, $update_docblocks = false)
{
if (!$class_context) {
throw new \InvalidArgumentException('TraitChecker::check must be called with a $class_context');

View File

@ -30,6 +30,23 @@ class Atomic extends Type
return $this->value;
}
public function toNamespacedString(array $aliased_classes, $this_class)
{
if ($this->value === $this_class) {
return $this_class;
}
if (isset($aliased_classes[$this->value])) {
return $aliased_classes[$this->value];
}
if ($this->isObjectType()) {
return '\\' . $this->value;
}
return $this->value;
}
/**
* @param Union $parent
* @return bool

View File

@ -28,7 +28,7 @@ class Generic extends Atomic
return $this->value .
'<' .
implode(
',',
', ',
array_map(
function ($type_param) {
return (string) $type_param;
@ -38,4 +38,23 @@ class Generic extends Atomic
) .
'>';
}
/**
* @return string
*/
public function toNamespacedString(array $aliased_classes, $this_class)
{
return $this->value .
'<' .
implode(
', ',
array_map(
function ($type_param) use ($aliased_classes, $this_class) {
return $type_param->toNamespacedString($aliased_classes, $this_class);
},
$this->type_params
)
) .
'>';
}
}

View File

@ -32,7 +32,7 @@ class ObjectLike extends Atomic
return $this->value .
'{' .
implode(
',',
', ',
array_map(
function ($name, $type) {
return $name . ':' . $type;
@ -43,4 +43,24 @@ class ObjectLike extends Atomic
) .
'}';
}
/**
* @return string
*/
public function toNamespacedString(array $aliased_classes, $this_class)
{
return $this->value .
'{' .
implode(
', ',
array_map(
function ($name, $type) use ($aliased_classes, $this_class) {
return $name . ':' . $type->toNamespacedString($aliased_classes, $this_class);
},
array_keys($this->properties),
$this->properties
)
) .
'}';
}
}

View File

@ -34,7 +34,20 @@ class Union extends Type
'|',
array_map(
function ($type) {
return (string) $type;
return (string)$type;
},
$this->types
)
);
}
public function toNamespacedString(array $aliased_classes, $this_class)
{
return implode(
'|',
array_map(
function ($type) use ($aliased_classes, $this_class) {
return $type->toNamespacedString($aliased_classes, $this_class);
},
$this->types
)