mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Break file manipulation out into Psalter
This commit is contained in:
parent
796a3c5066
commit
5bae869dc6
@ -16,7 +16,7 @@
|
|||||||
"openlss/lib-array2xml": "^0.0.10||^0.5.1",
|
"openlss/lib-array2xml": "^0.0.10||^0.5.1",
|
||||||
"muglug/package-versions-56": "1.2.3"
|
"muglug/package-versions-56": "1.2.3"
|
||||||
},
|
},
|
||||||
"bin": ["psalm"],
|
"bin": ["psalm", "psalter"],
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Psalm\\": "src/Psalm"
|
"Psalm\\": "src/Psalm"
|
||||||
|
2
psalter
Executable file
2
psalter
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php require_once __DIR__ . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'psalter.php';
|
@ -552,19 +552,15 @@ class CommentChecker
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($parsed_doc_comment['specials']) {
|
if ($parsed_doc_comment['specials']) {
|
||||||
$special_type_lengths = array_map('strlen', array_keys($parsed_doc_comment['specials']));
|
|
||||||
/** @var int */
|
|
||||||
$special_type_width = max($special_type_lengths) + 1;
|
|
||||||
|
|
||||||
$last_type = null;
|
$last_type = null;
|
||||||
|
|
||||||
foreach ($parsed_doc_comment['specials'] as $type => $lines) {
|
foreach ($parsed_doc_comment['specials'] as $type => $lines) {
|
||||||
if ($last_type !== null && ($last_type !== 'return' || $type !== 'psalm-return')) {
|
if ($last_type !== null && ($last_type !== 'return' || $last_type !== 'psalm-return')) {
|
||||||
$doc_comment_text .= $left_padding . ' *' . PHP_EOL;
|
$doc_comment_text .= $left_padding . ' *' . PHP_EOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$doc_comment_text .= $left_padding . ' * @' . str_pad($type, $special_type_width) . $line . PHP_EOL;
|
$doc_comment_text .= $left_padding . ' * @' . $type . ' ' . $line . PHP_EOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
$last_type = $type;
|
$last_type = $type;
|
||||||
|
@ -309,8 +309,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
new MismatchingDocblockParamType(
|
new MismatchingDocblockParamType(
|
||||||
'Parameter $' . $function_param->name . ' has wrong type \'' . $param_type .
|
'Parameter $' . $function_param->name . ' has wrong type \'' . $param_type .
|
||||||
'\', should be \'' . $signature_type . '\'',
|
'\', should be \'' . $signature_type . '\'',
|
||||||
$function_param->type_location,
|
$function_param->type_location
|
||||||
(string)$signature_type
|
|
||||||
),
|
),
|
||||||
$storage->suppressed_issues
|
$storage->suppressed_issues
|
||||||
)) {
|
)) {
|
||||||
@ -413,12 +412,19 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
$fleshed_out_signature_type
|
$fleshed_out_signature_type
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
if ($project_checker->alter_code
|
||||||
|
&& isset($project_checker->getIssuesToFix()['MismatchingDocblockReturnType'])
|
||||||
|
) {
|
||||||
|
$this->addOrUpdateReturnType($project_checker, $storage->signature_return_type, true);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
new MismatchingDocblockReturnType(
|
new MismatchingDocblockReturnType(
|
||||||
'Docblock has incorrect return type \'' . $storage->return_type .
|
'Docblock has incorrect return type \'' . $storage->return_type .
|
||||||
'\', should be \'' . $storage->signature_return_type . '\'',
|
'\', should be \'' . $storage->signature_return_type . '\'',
|
||||||
$storage->return_type_location,
|
$storage->return_type_location
|
||||||
(string) $storage->signature_return_type
|
|
||||||
),
|
),
|
||||||
$storage->suppressed_issues
|
$storage->suppressed_issues
|
||||||
)) {
|
)) {
|
||||||
@ -1164,35 +1170,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!$return_type && !$project_checker->add_docblocks && !$is_to_string) {
|
|
||||||
if ($this->function instanceof Closure) {
|
|
||||||
if (IssueBuffer::accepts(
|
|
||||||
new MissingClosureReturnType(
|
|
||||||
'Closure does not have a return type, expecting ' . $inferred_return_type,
|
|
||||||
new CodeLocation($this, $this->function, null, true)
|
|
||||||
),
|
|
||||||
$this->suppressed_issues
|
|
||||||
)) {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IssueBuffer::accepts(
|
|
||||||
new MissingReturnType(
|
|
||||||
'Method ' . $cased_method_id . ' does not have a return type' .
|
|
||||||
(!$inferred_return_type->isMixed() ? ', expecting ' . $inferred_return_type : ''),
|
|
||||||
new CodeLocation($this, $this->function, null, true)
|
|
||||||
),
|
|
||||||
$this->suppressed_issues
|
|
||||||
)) {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($is_to_string) {
|
if ($is_to_string) {
|
||||||
if (!$inferred_return_type->isMixed() && (string)$inferred_return_type !== 'string') {
|
if (!$inferred_return_type->isMixed() && (string)$inferred_return_type !== 'string') {
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
@ -1210,9 +1187,53 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$return_type) {
|
if (!$return_type) {
|
||||||
if (!$inferred_return_type->isMixed()) {
|
if ($this->function instanceof Closure) {
|
||||||
// $project_checker->add_docblocks is always true here
|
if ($project_checker->alter_code
|
||||||
$this->addDocblockReturnType($project_checker, $inferred_return_type);
|
&& isset($project_checker->getIssuesToFix()['MissingClosureReturnType'])
|
||||||
|
) {
|
||||||
|
if ($inferred_return_type->isMixed()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addOrUpdateReturnType($project_checker, $inferred_return_type);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IssueBuffer::accepts(
|
||||||
|
new MissingClosureReturnType(
|
||||||
|
'Closure does not have a return type, expecting ' . $inferred_return_type,
|
||||||
|
new CodeLocation($this, $this->function, null, true)
|
||||||
|
),
|
||||||
|
$this->suppressed_issues
|
||||||
|
)) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($project_checker->alter_code
|
||||||
|
&& isset($project_checker->getIssuesToFix()['MissingReturnType'])
|
||||||
|
) {
|
||||||
|
if ($inferred_return_type->isMixed()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addOrUpdateReturnType($project_checker, $inferred_return_type);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IssueBuffer::accepts(
|
||||||
|
new MissingReturnType(
|
||||||
|
'Method ' . $cased_method_id . ' does not have a return type' .
|
||||||
|
(!$inferred_return_type->isMixed() ? ', expecting ' . $inferred_return_type : ''),
|
||||||
|
new CodeLocation($this, $this->function, null, true)
|
||||||
|
),
|
||||||
|
$this->suppressed_issues
|
||||||
|
)) {
|
||||||
|
// fall through
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -1237,6 +1258,12 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($project_checker->alter_code && isset(getIssuesToFix()['InvalidReturnType'])) {
|
||||||
|
$this->addOrUpdateReturnType($project_checker, Type::getVoid());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
new InvalidReturnType(
|
new InvalidReturnType(
|
||||||
'No return statements were found for method ' . $cased_method_id .
|
'No return statements were found for method ' . $cased_method_id .
|
||||||
@ -1285,12 +1312,19 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
&& !$declared_return_type->isNullable()
|
&& !$declared_return_type->isNullable()
|
||||||
&& !$declared_return_type->isVoid()
|
&& !$declared_return_type->isVoid()
|
||||||
) {
|
) {
|
||||||
|
if ($project_checker->alter_code
|
||||||
|
&& isset($project_checker->getIssuesToFix()['NullableInferredReturnType'])
|
||||||
|
) {
|
||||||
|
$this->addOrUpdateReturnType($project_checker, $inferred_return_type);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
new NullableInferredReturnType(
|
new NullableInferredReturnType(
|
||||||
'The declared return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
|
'The declared return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
|
||||||
' is not nullable, but \'' . $inferred_return_type . '\' contains null',
|
' is not nullable, but \'' . $inferred_return_type . '\' contains null',
|
||||||
$return_type_location,
|
$return_type_location
|
||||||
(string) $inferred_return_type
|
|
||||||
),
|
),
|
||||||
$this->suppressed_issues
|
$this->suppressed_issues
|
||||||
)) {
|
)) {
|
||||||
@ -1302,12 +1336,19 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
&& !$declared_return_type->isFalsable()
|
&& !$declared_return_type->isFalsable()
|
||||||
&& !$declared_return_type->hasBool()
|
&& !$declared_return_type->hasBool()
|
||||||
) {
|
) {
|
||||||
|
if ($project_checker->alter_code
|
||||||
|
&& isset($project_checker->getIssuesToFix()['FalsableInferredReturnType'])
|
||||||
|
) {
|
||||||
|
$this->addOrUpdateReturnType($project_checker, $inferred_return_type);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
new FalsableInferredReturnType(
|
new FalsableInferredReturnType(
|
||||||
'The declared return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
|
'The declared return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
|
||||||
' does not allow false, but \'' . $inferred_return_type . '\' contains false',
|
' does not allow false, but \'' . $inferred_return_type . '\' contains false',
|
||||||
$return_type_location,
|
$return_type_location
|
||||||
(string) $inferred_return_type
|
|
||||||
),
|
),
|
||||||
$this->suppressed_issues
|
$this->suppressed_issues
|
||||||
)) {
|
)) {
|
||||||
@ -1338,12 +1379,19 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if ($project_checker->alter_code
|
||||||
|
&& isset($project_checker->getIssuesToFix()['InvalidReturnType'])
|
||||||
|
) {
|
||||||
|
$this->addOrUpdateReturnType($project_checker, $inferred_return_type);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
new InvalidReturnType(
|
new InvalidReturnType(
|
||||||
'The declared return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
|
'The declared return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
|
||||||
' is incorrect, got \'' . $inferred_return_type . '\'',
|
' is incorrect, got \'' . $inferred_return_type . '\'',
|
||||||
$return_type_location,
|
$return_type_location
|
||||||
(string) $inferred_return_type
|
|
||||||
),
|
),
|
||||||
$this->suppressed_issues
|
$this->suppressed_issues
|
||||||
)) {
|
)) {
|
||||||
@ -1351,6 +1399,14 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} elseif (!$inferred_return_type->isNullable() && $declared_return_type->isNullable()) {
|
} elseif (!$inferred_return_type->isNullable() && $declared_return_type->isNullable()) {
|
||||||
|
if ($project_checker->alter_code
|
||||||
|
&& isset($project_checker->getIssuesToFix()['LessSpecificReturnType'])
|
||||||
|
) {
|
||||||
|
$this->addOrUpdateReturnType($project_checker, $inferred_return_type);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
new LessSpecificReturnType(
|
new LessSpecificReturnType(
|
||||||
'The inferred return type \'' . $inferred_return_type . '\' for ' . $cased_method_id .
|
'The inferred return type \'' . $inferred_return_type . '\' for ' . $cased_method_id .
|
||||||
@ -1368,20 +1424,29 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Type\Union $inferred_return_type
|
* @param bool $docblock_only
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function addDocblockReturnType(ProjectChecker $project_checker, Type\Union $inferred_return_type)
|
private function addOrUpdateReturnType(
|
||||||
{
|
ProjectChecker $project_checker,
|
||||||
|
Type\Union $inferred_return_type,
|
||||||
|
$docblock_only = false
|
||||||
|
) {
|
||||||
$manipulator = FunctionDocblockManipulator::getForFunction(
|
$manipulator = FunctionDocblockManipulator::getForFunction(
|
||||||
$project_checker,
|
$project_checker,
|
||||||
$this->source->getFilePath(),
|
$this->source->getFilePath(),
|
||||||
$this->getMethodId(),
|
$this->getMethodId(),
|
||||||
$this->function
|
$this->function
|
||||||
);
|
);
|
||||||
|
$manipulator->setReturnType(
|
||||||
$manipulator->setDocblockReturnType(
|
!$docblock_only && $project_checker->php_major_version >= 7
|
||||||
|
? $inferred_return_type->toPhpString(
|
||||||
|
$this->source->getAliasedClassesFlipped(),
|
||||||
|
$this->source->getFQCLN(),
|
||||||
|
$project_checker->php_major_version,
|
||||||
|
$project_checker->php_minor_version
|
||||||
|
) : null,
|
||||||
$inferred_return_type->toNamespacedString(
|
$inferred_return_type->toNamespacedString(
|
||||||
$this->source->getAliasedClassesFlipped(),
|
$this->source->getAliasedClassesFlipped(),
|
||||||
$this->source->getFQCLN(),
|
$this->source->getFQCLN(),
|
||||||
@ -1391,7 +1456,8 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
|||||||
$this->source->getAliasedClassesFlipped(),
|
$this->source->getAliasedClassesFlipped(),
|
||||||
$this->source->getFQCLN(),
|
$this->source->getFQCLN(),
|
||||||
true
|
true
|
||||||
)
|
),
|
||||||
|
$inferred_return_type->canBeFullyExpressedInPhp()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ class ProjectChecker
|
|||||||
/**
|
/**
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
public $add_docblocks = false;
|
public $alter_code = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var bool
|
* @var bool
|
||||||
@ -259,6 +259,16 @@ class ProjectChecker
|
|||||||
*/
|
*/
|
||||||
private $issues_to_fix = [];
|
private $issues_to_fix = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $php_major_version = PHP_MAJOR_VERSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $php_minor_version = PHP_MINOR_VERSION;
|
||||||
|
|
||||||
const TYPE_CONSOLE = 'console';
|
const TYPE_CONSOLE = 'console';
|
||||||
const TYPE_JSON = 'json';
|
const TYPE_JSON = 'json';
|
||||||
const TYPE_EMACS = 'emacs';
|
const TYPE_EMACS = 'emacs';
|
||||||
@ -1004,7 +1014,7 @@ class ProjectChecker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->replace_code || $this->add_docblocks || $this->issues_to_fix) {
|
if ($this->replace_code || $this->alter_code || $this->issues_to_fix) {
|
||||||
foreach ($this->files_to_report as $file_path) {
|
foreach ($this->files_to_report as $file_path) {
|
||||||
$this->updateFile($file_path, true);
|
$this->updateFile($file_path, true);
|
||||||
}
|
}
|
||||||
@ -1019,7 +1029,7 @@ class ProjectChecker
|
|||||||
*/
|
*/
|
||||||
public function updateFile($file_path, $output_changes = false)
|
public function updateFile($file_path, $output_changes = false)
|
||||||
{
|
{
|
||||||
if ($this->add_docblocks) {
|
if ($this->alter_code) {
|
||||||
$new_return_type_manipulations = FunctionDocblockManipulator::getManipulationsForFile($file_path);
|
$new_return_type_manipulations = FunctionDocblockManipulator::getManipulationsForFile($file_path);
|
||||||
} else {
|
} else {
|
||||||
$new_return_type_manipulations = [];
|
$new_return_type_manipulations = [];
|
||||||
@ -2082,19 +2092,16 @@ class ProjectChecker
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function addDocblocksAfterCompletion()
|
public function alterCodeAfterCompletion($php_major_version, $php_minor_version)
|
||||||
{
|
{
|
||||||
$this->add_docblocks = true;
|
$this->alter_code = true;
|
||||||
}
|
$this->php_major_version = $php_major_version;
|
||||||
|
$this->php_minor_version = $php_minor_version;
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function replaceCodeAfterCompletion()
|
|
||||||
{
|
|
||||||
$this->replace_code = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2102,13 +2109,15 @@ class ProjectChecker
|
|||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function fixIssuesAfterCompletion(array $issues)
|
public function setIssuesToFix(array $issues)
|
||||||
{
|
{
|
||||||
$this->issues_to_fix = $issues;
|
$this->issues_to_fix = $issues;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, bool>
|
* @return array<string, bool>
|
||||||
|
*
|
||||||
|
* @psalm-suppress PossiblyUnusedMethod - need to fix #422
|
||||||
*/
|
*/
|
||||||
public function getIssuesToFix()
|
public function getIssuesToFix()
|
||||||
{
|
{
|
||||||
|
@ -1023,7 +1023,6 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
|||||||
|
|
||||||
if ($storage->return_type
|
if ($storage->return_type
|
||||||
&& !$storage->return_type->isMixed()
|
&& !$storage->return_type->isMixed()
|
||||||
&& !$project_checker->add_docblocks
|
|
||||||
) {
|
) {
|
||||||
$inferred_type = ExpressionChecker::fleshOutType(
|
$inferred_type = ExpressionChecker::fleshOutType(
|
||||||
$project_checker,
|
$project_checker,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
namespace Psalm\FileManipulation;
|
namespace Psalm\FileManipulation;
|
||||||
|
|
||||||
use PhpParser\Node\Expr\Closure;
|
use PhpParser\Node\Expr\Closure;
|
||||||
|
use PhpParser\Node\FunctionLike;
|
||||||
use PhpParser\Node\Stmt\ClassMethod;
|
use PhpParser\Node\Stmt\ClassMethod;
|
||||||
use PhpParser\Node\Stmt\Function_;
|
use PhpParser\Node\Stmt\Function_;
|
||||||
use Psalm\Checker\CommentChecker;
|
use Psalm\Checker\CommentChecker;
|
||||||
@ -28,6 +29,21 @@ class FunctionDocblockManipulator
|
|||||||
/** @var int */
|
/** @var int */
|
||||||
private $docblock_end;
|
private $docblock_end;
|
||||||
|
|
||||||
|
/** @var int */
|
||||||
|
private $return_typehint_area_start;
|
||||||
|
|
||||||
|
/** @var ?int */
|
||||||
|
private $return_typehint_start;
|
||||||
|
|
||||||
|
/** @var ?int */
|
||||||
|
private $return_typehint_end;
|
||||||
|
|
||||||
|
/** @var ?string */
|
||||||
|
private $new_php_return_type;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $return_type_is_php_compatible = false;
|
||||||
|
|
||||||
/** @var ?string */
|
/** @var ?string */
|
||||||
private $new_phpdoc_return_type;
|
private $new_phpdoc_return_type;
|
||||||
|
|
||||||
@ -44,8 +60,12 @@ class FunctionDocblockManipulator
|
|||||||
*
|
*
|
||||||
* @return self
|
* @return self
|
||||||
*/
|
*/
|
||||||
public static function getForFunction(ProjectChecker $project_checker, $file_path, $function_id, $stmt)
|
public static function getForFunction(
|
||||||
{
|
ProjectChecker $project_checker,
|
||||||
|
$file_path,
|
||||||
|
$function_id,
|
||||||
|
FunctionLike $stmt
|
||||||
|
) {
|
||||||
if (isset(self::$manipulators[$file_path][$function_id])) {
|
if (isset(self::$manipulators[$file_path][$function_id])) {
|
||||||
return self::$manipulators[$file_path][$function_id];
|
return self::$manipulators[$file_path][$function_id];
|
||||||
}
|
}
|
||||||
@ -62,32 +82,99 @@ class FunctionDocblockManipulator
|
|||||||
* @param string $file_path
|
* @param string $file_path
|
||||||
* @param Closure|Function_|ClassMethod $stmt
|
* @param Closure|Function_|ClassMethod $stmt
|
||||||
*/
|
*/
|
||||||
private function __construct($file_path, $stmt, ProjectChecker $project_checker)
|
private function __construct($file_path, FunctionLike $stmt, ProjectChecker $project_checker)
|
||||||
{
|
{
|
||||||
$this->stmt = $stmt;
|
$this->stmt = $stmt;
|
||||||
$docblock = $stmt->getDocComment();
|
$docblock = $stmt->getDocComment();
|
||||||
$this->docblock_start = $docblock ? $docblock->getFilePos() : (int)$stmt->getAttribute('startFilePos');
|
$this->docblock_start = $docblock ? $docblock->getFilePos() : (int)$stmt->getAttribute('startFilePos');
|
||||||
$this->docblock_end = (int)$stmt->getAttribute('startFilePos');
|
$this->docblock_end = $function_start = (int)$stmt->getAttribute('startFilePos');
|
||||||
|
$function_end = (int)$stmt->getAttribute('endFilePos');
|
||||||
|
|
||||||
|
$file_contents = $project_checker->getFileContents($file_path);
|
||||||
|
|
||||||
|
$last_arg_position = $stmt->params
|
||||||
|
? (int) $stmt->params[count($stmt->params) - 1]->getAttribute('endFilePos')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if ($stmt instanceof Closure && $stmt->uses) {
|
||||||
|
$last_arg_position = (int) $stmt->uses[count($stmt->uses) - 1]->getAttribute('endFilePos');
|
||||||
|
}
|
||||||
|
|
||||||
|
$end_bracket_position = (int) strpos($file_contents, ')', $last_arg_position ?: $function_start);
|
||||||
|
|
||||||
|
$this->return_typehint_area_start = $end_bracket_position + 1;
|
||||||
|
|
||||||
|
$function_code = substr($file_contents, $function_start, $function_end);
|
||||||
|
|
||||||
|
$function_code_after_bracket = substr($function_code, $end_bracket_position + 1 - $function_start);
|
||||||
|
|
||||||
|
// do a little parsing here
|
||||||
|
/** @var array<int, string> */
|
||||||
|
$chars = str_split($function_code_after_bracket);
|
||||||
|
|
||||||
|
foreach ($chars as $i => $char) {
|
||||||
|
switch ($char) {
|
||||||
|
case PHP_EOL:
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case ':':
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case '/':
|
||||||
|
// @todo handle comments in this area
|
||||||
|
throw new \UnexpectedValueException('Not expecting comments where return types should live');
|
||||||
|
|
||||||
|
case '{':
|
||||||
|
break 2;
|
||||||
|
|
||||||
|
case '?':
|
||||||
|
$this->return_typehint_start = $i + $end_bracket_position + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/\w/', $char)) {
|
||||||
|
if ($this->return_typehint_start === null) {
|
||||||
|
$this->return_typehint_start = $i + $end_bracket_position + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/\w/', $chars[$i + 1])) {
|
||||||
|
$this->return_typehint_end = $i + $end_bracket_position + 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$preceding_newline_pos = strrpos($file_contents, PHP_EOL, $this->docblock_end - strlen($file_contents));
|
||||||
|
|
||||||
|
if ($preceding_newline_pos === false) {
|
||||||
|
$this->indentation = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first_line = substr($file_contents, $preceding_newline_pos + 1, $this->docblock_end - $preceding_newline_pos);
|
||||||
|
|
||||||
$file_lines = explode(PHP_EOL, $project_checker->getFileContents($file_path));
|
|
||||||
$first_line = $file_lines[$stmt->getLine() - 1];
|
|
||||||
$this->indentation = str_replace(ltrim($first_line), '', $first_line);
|
$this->indentation = str_replace(ltrim($first_line), '', $first_line);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the new docblock return type
|
* Sets the new docblock return type
|
||||||
*
|
*
|
||||||
|
* @param ?string $php_type
|
||||||
* @param string $new_type
|
* @param string $new_type
|
||||||
* @param string $phpdoc_type
|
* @param string $phpdoc_type
|
||||||
|
* @param bool $is_php_compatible
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function setDocblockReturnType($new_type, $phpdoc_type)
|
public function setReturnType($php_type, $new_type, $phpdoc_type, $is_php_compatible)
|
||||||
{
|
{
|
||||||
$new_type = str_replace(['<mixed, mixed>', '<empty, empty>'], '', $new_type);
|
$new_type = str_replace(['<mixed, mixed>', '<empty, empty>'], '', $new_type);
|
||||||
|
|
||||||
|
$this->new_php_return_type = $php_type;
|
||||||
$this->new_phpdoc_return_type = $phpdoc_type;
|
$this->new_phpdoc_return_type = $phpdoc_type;
|
||||||
$this->new_psalm_return_type = $new_type;
|
$this->new_psalm_return_type = $new_type;
|
||||||
|
$this->return_type_is_php_compatible = $is_php_compatible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,11 +218,32 @@ class FunctionDocblockManipulator
|
|||||||
$file_manipulations = [];
|
$file_manipulations = [];
|
||||||
|
|
||||||
foreach (self::$ordered_manipulators[$file_path] as $manipulator) {
|
foreach (self::$ordered_manipulators[$file_path] as $manipulator) {
|
||||||
$file_manipulations[$manipulator->docblock_start] = new FileManipulation(
|
if ($manipulator->new_php_return_type) {
|
||||||
$manipulator->docblock_start,
|
if ($manipulator->return_typehint_start && $manipulator->return_typehint_end) {
|
||||||
$manipulator->docblock_end,
|
$file_manipulations[$manipulator->return_typehint_start] = new FileManipulation(
|
||||||
$manipulator->getDocblock()
|
$manipulator->return_typehint_start,
|
||||||
);
|
$manipulator->return_typehint_end,
|
||||||
|
$manipulator->new_php_return_type
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$file_manipulations[$manipulator->return_typehint_area_start] = new FileManipulation(
|
||||||
|
$manipulator->return_typehint_area_start,
|
||||||
|
$manipulator->return_typehint_area_start,
|
||||||
|
' : ' . $manipulator->new_php_return_type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$manipulator->new_php_return_type
|
||||||
|
|| !$manipulator->return_type_is_php_compatible
|
||||||
|
|| $manipulator->docblock_start !== $manipulator->docblock_end
|
||||||
|
) {
|
||||||
|
$file_manipulations[$manipulator->docblock_start] = new FileManipulation(
|
||||||
|
$manipulator->docblock_start,
|
||||||
|
$manipulator->docblock_end,
|
||||||
|
$manipulator->getDocblock()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $file_manipulations;
|
return $file_manipulations;
|
||||||
|
@ -1,34 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Psalm\Issue;
|
namespace Psalm\Issue;
|
||||||
|
|
||||||
use Psalm\CodeLocation;
|
|
||||||
|
|
||||||
abstract class FixableCodeIssue extends CodeIssue
|
abstract class FixableCodeIssue extends CodeIssue
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var ?string
|
|
||||||
*/
|
|
||||||
protected $replacement_text;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $message
|
|
||||||
* @param CodeLocation $code_location
|
|
||||||
* @param string|null $replacement_text
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
$message,
|
|
||||||
CodeLocation $code_location,
|
|
||||||
$replacement_text = null
|
|
||||||
) {
|
|
||||||
parent::__construct($message, $code_location);
|
|
||||||
$this->replacement_text = $replacement_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ?string
|
|
||||||
*/
|
|
||||||
public function getReplacementText()
|
|
||||||
{
|
|
||||||
return $this->replacement_text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ namespace Psalm;
|
|||||||
|
|
||||||
use LSS\Array2XML;
|
use LSS\Array2XML;
|
||||||
use Psalm\Checker\ProjectChecker;
|
use Psalm\Checker\ProjectChecker;
|
||||||
use Psalm\FileManipulation\FileManipulation;
|
|
||||||
use Psalm\FileManipulation\FileManipulationBuffer;
|
|
||||||
use Psalm\Issue\CodeIssue;
|
use Psalm\Issue\CodeIssue;
|
||||||
|
|
||||||
class IssueBuffer
|
class IssueBuffer
|
||||||
@ -81,24 +79,13 @@ class IssueBuffer
|
|||||||
public static function add(CodeIssue $e)
|
public static function add(CodeIssue $e)
|
||||||
{
|
{
|
||||||
$config = Config::getInstance();
|
$config = Config::getInstance();
|
||||||
$project_checker = ProjectChecker::getInstance();
|
|
||||||
|
|
||||||
$fqcn_parts = explode('\\', get_class($e));
|
$fqcn_parts = explode('\\', get_class($e));
|
||||||
$issue_type = array_pop($fqcn_parts);
|
$issue_type = array_pop($fqcn_parts);
|
||||||
|
|
||||||
$issues_to_fix = \Psalm\Checker\ProjectChecker::getInstance()->getIssuesToFix();
|
$project_checker = ProjectChecker::getInstance();
|
||||||
|
|
||||||
if (isset($issues_to_fix[$issue_type])
|
|
||||||
&& $e instanceof \Psalm\Issue\FixableCodeIssue
|
|
||||||
&& $replacement_text = $e->getReplacementText()
|
|
||||||
) {
|
|
||||||
$code_location = $e->getLocation();
|
|
||||||
$bounds = $code_location->getSelectionBounds();
|
|
||||||
FileManipulationBuffer::add(
|
|
||||||
$e->getFilePath(),
|
|
||||||
[new FileManipulation($bounds[0], $bounds[1], $replacement_text)]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if ($project_checker->alter_code) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,6 +251,21 @@ abstract class Atomic
|
|||||||
return $this->getKey();
|
return $this->getKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
abstract public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
abstract public function canBeFullyExpressedInPhp();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
|
@ -80,6 +80,24 @@ class ObjectLike extends \Psalm\Type\Atomic
|
|||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return $this->getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Union
|
* @return Union
|
||||||
*/
|
*/
|
||||||
|
@ -3,4 +3,24 @@ namespace Psalm\Type\Atomic;
|
|||||||
|
|
||||||
abstract class Scalar extends \Psalm\Type\Atomic
|
abstract class Scalar extends \Psalm\Type\Atomic
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return $php_major_version >= 7 ? $this->getKey() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,4 +17,12 @@ abstract class T extends TString
|
|||||||
{
|
{
|
||||||
$this->typeof = $typeof;
|
$this->typeof = $typeof;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,4 +27,22 @@ class TArray extends \Psalm\Type\Atomic implements Generic
|
|||||||
{
|
{
|
||||||
return 'array';
|
return 'array';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return $this->getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return $this->type_params[0]->isMixed() && $this->type_params[1]->isMixed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,17 @@ class TBool extends Scalar
|
|||||||
{
|
{
|
||||||
return 'bool';
|
return 'bool';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return $php_major_version >= 7 ? 'bool' : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,22 @@ class TCallable extends \Psalm\Type\Atomic
|
|||||||
{
|
{
|
||||||
return 'callable';
|
return 'callable';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,17 @@ class TEmpty extends Scalar
|
|||||||
{
|
{
|
||||||
return 'empty';
|
return 'empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,9 @@ class TFalse extends TBool
|
|||||||
{
|
{
|
||||||
return 'false';
|
return 'false';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,4 +17,12 @@ class TGenericObject extends TNamedObject implements Generic
|
|||||||
$this->value = $value;
|
$this->value = $value;
|
||||||
$this->type_params = $type_params;
|
$this->type_params = $type_params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,22 @@ class TMixed extends \Psalm\Type\Atomic
|
|||||||
{
|
{
|
||||||
return 'mixed';
|
return 'mixed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,24 @@ class TNamedObject extends Atomic
|
|||||||
return '\\' . $this->value;
|
return '\\' . $this->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return $this->toNamespacedString($aliased_classes, $this_class, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param TNamedObject $type
|
* @param TNamedObject $type
|
||||||
*
|
*
|
||||||
|
@ -15,4 +15,22 @@ class TNull extends \Psalm\Type\Atomic
|
|||||||
{
|
{
|
||||||
return 'null';
|
return 'null';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,17 @@ class TNumeric extends Scalar
|
|||||||
{
|
{
|
||||||
return 'numeric';
|
return 'numeric';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,12 @@ class TNumericString extends TString
|
|||||||
{
|
{
|
||||||
return $this->getKey();
|
return $this->getKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,22 @@ class TObject extends \Psalm\Type\Atomic
|
|||||||
{
|
{
|
||||||
return 'object';
|
return 'object';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return $php_major_version >= 7 && $php_minor_version >= 2 ? $this->getKey() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,22 @@ class TResource extends \Psalm\Type\Atomic
|
|||||||
{
|
{
|
||||||
return 'resource';
|
return 'resource';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,22 @@ class TScalar extends Scalar
|
|||||||
{
|
{
|
||||||
return 'scalar';
|
return 'scalar';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,9 @@ class TTrue extends TBool
|
|||||||
{
|
{
|
||||||
return 'true';
|
return 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,22 @@ class TVoid extends \Psalm\Type\Atomic
|
|||||||
{
|
{
|
||||||
return 'void';
|
return 'void';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
return $php_major_version >= 7 && $php_minor_version >= 1 ? $this->getKey() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,78 @@ class Union
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $aliased_classes
|
||||||
|
* @param string|null $this_class
|
||||||
|
* @param int $php_major_version
|
||||||
|
* @param int $php_minor_version
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
*/
|
||||||
|
public function toPhpString(array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
|
||||||
|
{
|
||||||
|
$nullable = false;
|
||||||
|
|
||||||
|
if (count($this->types) > 2
|
||||||
|
|| (
|
||||||
|
count($this->types) === 2
|
||||||
|
&& (!isset($this->types['null'])
|
||||||
|
|| $php_major_version < 7
|
||||||
|
|| $php_minor_version < 1)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$types = $this->types;
|
||||||
|
|
||||||
|
if (isset($types['null'])) {
|
||||||
|
unset($types['null']);
|
||||||
|
|
||||||
|
$nullable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$atomic_type = array_values($types)[0];
|
||||||
|
|
||||||
|
$atomic_type_string = $atomic_type->toPhpString(
|
||||||
|
$aliased_classes,
|
||||||
|
$this_class,
|
||||||
|
$php_major_version,
|
||||||
|
$php_minor_version
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($atomic_type_string) {
|
||||||
|
return ($nullable ? '?' : '') . $atomic_type_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canBeFullyExpressedInPhp()
|
||||||
|
{
|
||||||
|
if (count($this->types) > 2
|
||||||
|
|| (
|
||||||
|
count($this->types) === 2
|
||||||
|
&& !isset($this->types['null'])
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$types = $this->types;
|
||||||
|
|
||||||
|
if (isset($types['null'])) {
|
||||||
|
unset($types['null']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$atomic_type = array_values($types)[0];
|
||||||
|
|
||||||
|
return $atomic_type->canBeFullyExpressedInPhp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
|
133
src/command_functions.php
Normal file
133
src/command_functions.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $current_dir
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function requireAutoloaders($current_dir)
|
||||||
|
{
|
||||||
|
$autoload_roots = [$current_dir];
|
||||||
|
|
||||||
|
$psalm_dir = dirname(__DIR__);
|
||||||
|
|
||||||
|
if (realpath($psalm_dir) !== realpath($current_dir)) {
|
||||||
|
$autoload_roots[] = $psalm_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoload_files = [];
|
||||||
|
|
||||||
|
foreach ($autoload_roots as $autoload_root) {
|
||||||
|
$has_autoloader = false;
|
||||||
|
|
||||||
|
$nested_autoload_file = dirname(dirname($autoload_root)) . DIRECTORY_SEPARATOR . 'autoload.php';
|
||||||
|
|
||||||
|
if (file_exists($nested_autoload_file)) {
|
||||||
|
$autoload_files[] = realpath($nested_autoload_file);
|
||||||
|
$has_autoloader = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendor_autoload_file = $autoload_root . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
|
||||||
|
|
||||||
|
if (file_exists($vendor_autoload_file)) {
|
||||||
|
$autoload_files[] = realpath($vendor_autoload_file);
|
||||||
|
$has_autoloader = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$has_autoloader) {
|
||||||
|
$error_message = 'Could not find any composer autoloaders in ' . $autoload_root;
|
||||||
|
|
||||||
|
if (!isset($options['r'])) {
|
||||||
|
$error_message .= PHP_EOL . 'Add a --root=[your/project/directory] flag '
|
||||||
|
. 'to specify a particular project to run Psalm on.';
|
||||||
|
}
|
||||||
|
|
||||||
|
die($error_message . PHP_EOL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($autoload_files as $file) {
|
||||||
|
/** @psalm-suppress UnresolvableInclude */
|
||||||
|
require_once $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|string[]|null $f_paths
|
||||||
|
*
|
||||||
|
* @return string[]|null
|
||||||
|
*/
|
||||||
|
function getPathsToCheck($f_paths)
|
||||||
|
{
|
||||||
|
global $argv;
|
||||||
|
|
||||||
|
$paths_to_check = [];
|
||||||
|
|
||||||
|
if ($f_paths) {
|
||||||
|
$input_paths = is_array($f_paths) ? $f_paths : [$f_paths];
|
||||||
|
} else {
|
||||||
|
$input_paths = $argv ? $argv : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input_paths) {
|
||||||
|
$filtered_input_paths = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($input_paths); ++$i) {
|
||||||
|
/** @var string */
|
||||||
|
$input_path = $input_paths[$i];
|
||||||
|
|
||||||
|
if (realpath($input_path) === realpath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'psalm')
|
||||||
|
|| realpath($input_path) === realpath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'psalter')
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input_path[0] === '-' && strlen($input_path) === 2) {
|
||||||
|
if ($input_path[1] === 'c' || $input_path[1] === 'f') {
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input_path[0] === '-' && $input_path[2] === '=') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (substr($input_path, 0, 2) === '--' && strlen($input_path) > 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filtered_input_paths[] = $input_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_set_blocking(STDIN, false);
|
||||||
|
|
||||||
|
if ($filtered_input_paths === ['-'] && $stdin = fgets(STDIN)) {
|
||||||
|
$filtered_input_paths = preg_split('/\s+/', trim($stdin));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($filtered_input_paths as $i => $path_to_check) {
|
||||||
|
if ($path_to_check[0] === '-') {
|
||||||
|
die('Invalid usage, expecting psalm [options] [file...]' . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($path_to_check)) {
|
||||||
|
die('Cannot locate ' . $path_to_check . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path_to_check = realpath($path_to_check);
|
||||||
|
|
||||||
|
if (!$path_to_check) {
|
||||||
|
die('Error getting realpath for file' . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paths_to_check[] = $path_to_check;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$paths_to_check) {
|
||||||
|
$paths_to_check = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paths_to_check;
|
||||||
|
}
|
161
src/psalm.php
161
src/psalm.php
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
require_once('command_functions.php');
|
||||||
|
|
||||||
use Psalm\Checker\ProjectChecker;
|
use Psalm\Checker\ProjectChecker;
|
||||||
use Psalm\Config;
|
use Psalm\Config;
|
||||||
@ -14,10 +15,9 @@ $options = getopt(
|
|||||||
'f:mhvc:ir:',
|
'f:mhvc:ir:',
|
||||||
[
|
[
|
||||||
'help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff',
|
'help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff',
|
||||||
'file:', 'self-check', 'add-docblocks', 'output-format:',
|
'self-check', 'output-format:', 'report:', 'find-dead-code', 'init',
|
||||||
'find-dead-code', 'init', 'find-references-to:', 'root:', 'threads:',
|
'find-references-to:', 'root:', 'threads:', 'clear-cache', 'no-cache',
|
||||||
'report:', 'clear-cache', 'no-cache', 'version', 'plugin:', 'replace-code',
|
'version', 'plugin:',
|
||||||
'fix-code',
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -82,9 +82,6 @@ Options:
|
|||||||
--self-check
|
--self-check
|
||||||
Psalm checks itself
|
Psalm checks itself
|
||||||
|
|
||||||
--add-docblocks
|
|
||||||
Adds correct docblock return types to the given file(s)
|
|
||||||
|
|
||||||
--output-format=console
|
--output-format=console
|
||||||
Changes the output format. Possible values: console, json, xml
|
Changes the output format. Possible values: console, json, xml
|
||||||
|
|
||||||
@ -111,12 +108,6 @@ Options:
|
|||||||
--plugin=PATH
|
--plugin=PATH
|
||||||
Executes a plugin, an alternative to using the Psalm config
|
Executes a plugin, an alternative to using the Psalm config
|
||||||
|
|
||||||
--replace-code
|
|
||||||
Processes any plugin code replacements and updates the code accordingly
|
|
||||||
|
|
||||||
--fix-issues=IssueType1,IssueType2
|
|
||||||
If any issues that can be fixed automatically, Psalm will update the codebase
|
|
||||||
|
|
||||||
HELP;
|
HELP;
|
||||||
|
|
||||||
exit;
|
exit;
|
||||||
@ -142,49 +133,7 @@ if (isset($options['r']) && is_string($options['r'])) {
|
|||||||
$current_dir = $root_path . DIRECTORY_SEPARATOR;
|
$current_dir = $root_path . DIRECTORY_SEPARATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
$autoload_roots = [$current_dir];
|
requireAutoloaders($current_dir);
|
||||||
|
|
||||||
$psalm_dir = dirname(__DIR__);
|
|
||||||
|
|
||||||
if (realpath($psalm_dir) !== realpath($current_dir)) {
|
|
||||||
$autoload_roots[] = $psalm_dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
$autoload_files = [];
|
|
||||||
|
|
||||||
foreach ($autoload_roots as $autoload_root) {
|
|
||||||
$has_autoloader = false;
|
|
||||||
|
|
||||||
$nested_autoload_file = dirname(dirname($autoload_root)) . DIRECTORY_SEPARATOR . 'autoload.php';
|
|
||||||
|
|
||||||
if (file_exists($nested_autoload_file)) {
|
|
||||||
$autoload_files[] = realpath($nested_autoload_file);
|
|
||||||
$has_autoloader = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$vendor_autoload_file = $autoload_root . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
|
|
||||||
|
|
||||||
if (file_exists($vendor_autoload_file)) {
|
|
||||||
$autoload_files[] = realpath($vendor_autoload_file);
|
|
||||||
$has_autoloader = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$has_autoloader) {
|
|
||||||
$error_message = 'Could not find any composer autoloaders in ' . $autoload_root;
|
|
||||||
|
|
||||||
if (!isset($options['r'])) {
|
|
||||||
$error_message .=
|
|
||||||
PHP_EOL . 'Add a --root=[your/project/directory] flag to specify a particular project to run Psalm on.';
|
|
||||||
}
|
|
||||||
|
|
||||||
die($error_message . PHP_EOL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($autoload_files as $file) {
|
|
||||||
/** @psalm-suppress UnresolvableInclude */
|
|
||||||
require_once $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array_key_exists('v', $options)) {
|
if (array_key_exists('v', $options)) {
|
||||||
/** @var string */
|
/** @var string */
|
||||||
@ -263,78 +212,11 @@ if (isset($options['i'])) {
|
|||||||
exit('Config file created successfully. Please re-run psalm.' . PHP_EOL);
|
exit('Config file created successfully. Please re-run psalm.' . PHP_EOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get vars from options
|
|
||||||
$debug = array_key_exists('debug', $options);
|
|
||||||
|
|
||||||
if (isset($options['f'])) {
|
|
||||||
$input_paths = is_array($options['f']) ? $options['f'] : [$options['f']];
|
|
||||||
} else {
|
|
||||||
$input_paths = $argv ? $argv : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$output_format = isset($options['output-format']) && is_string($options['output-format'])
|
$output_format = isset($options['output-format']) && is_string($options['output-format'])
|
||||||
? $options['output-format']
|
? $options['output-format']
|
||||||
: ProjectChecker::TYPE_CONSOLE;
|
: ProjectChecker::TYPE_CONSOLE;
|
||||||
|
|
||||||
$paths_to_check = null;
|
$paths_to_check = getPathsToCheck(isset($options['f']) ? $options['f'] : null);
|
||||||
|
|
||||||
if ($input_paths) {
|
|
||||||
$filtered_input_paths = [];
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($input_paths); ++$i) {
|
|
||||||
/** @var string */
|
|
||||||
$input_path = $input_paths[$i];
|
|
||||||
|
|
||||||
if (realpath($input_path) === realpath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'psalm')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($input_path[0] === '-' && strlen($input_path) === 2) {
|
|
||||||
if ($input_path[1] === 'c' || $input_path[1] === 'f') {
|
|
||||||
++$i;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($input_path[0] === '-' && $input_path[2] === '=') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (substr($input_path, 0, 2) === '--' && strlen($input_path) > 2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$filtered_input_paths[] = $input_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
stream_set_blocking(STDIN, false);
|
|
||||||
|
|
||||||
if ($filtered_input_paths === ['-'] && $stdin = fgets(STDIN)) {
|
|
||||||
$filtered_input_paths = preg_split('/\s+/', trim($stdin));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($filtered_input_paths as $i => $path_to_check) {
|
|
||||||
if ($path_to_check[0] === '-') {
|
|
||||||
die('Invalid usage, expecting psalm [options] [file...]' . PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file_exists($path_to_check)) {
|
|
||||||
die('Cannot locate ' . $path_to_check . PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
$path_to_check = realpath($path_to_check);
|
|
||||||
|
|
||||||
if (!$path_to_check) {
|
|
||||||
die('Error getting realpath for file' . PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
$paths_to_check[] = $path_to_check;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$paths_to_check) {
|
|
||||||
$paths_to_check = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$plugins = [];
|
$plugins = [];
|
||||||
|
|
||||||
@ -353,11 +235,9 @@ if ($path_to_config === false) {
|
|||||||
die('Could not resolve path to config ' . (string)$options['c'] . PHP_EOL);
|
die('Could not resolve path to config ' . (string)$options['c'] . PHP_EOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
$use_color = !array_key_exists('m', $options);
|
|
||||||
|
|
||||||
$show_info = isset($options['show-info'])
|
$show_info = isset($options['show-info'])
|
||||||
? $options['show-info'] !== 'false' && $options['show-info'] !== '0'
|
? $options['show-info'] !== 'false' && $options['show-info'] !== '0'
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
$is_diff = isset($options['diff']);
|
$is_diff = isset($options['diff']);
|
||||||
|
|
||||||
@ -367,8 +247,6 @@ $find_references_to = isset($options['find-references-to']) && is_string($option
|
|||||||
? $options['find-references-to']
|
? $options['find-references-to']
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
$add_docblocks = isset($options['add-docblocks']);
|
|
||||||
|
|
||||||
$threads = isset($options['threads']) ? (int)$options['threads'] : 1;
|
$threads = isset($options['threads']) ? (int)$options['threads'] : 1;
|
||||||
|
|
||||||
$cache_provider = isset($options['no-cache'])
|
$cache_provider = isset($options['no-cache'])
|
||||||
@ -378,11 +256,11 @@ $cache_provider = isset($options['no-cache'])
|
|||||||
$project_checker = new ProjectChecker(
|
$project_checker = new ProjectChecker(
|
||||||
new Psalm\Provider\FileProvider(),
|
new Psalm\Provider\FileProvider(),
|
||||||
$cache_provider,
|
$cache_provider,
|
||||||
$use_color,
|
!array_key_exists('m', $options),
|
||||||
$show_info,
|
$show_info,
|
||||||
$output_format,
|
$output_format,
|
||||||
$threads,
|
$threads,
|
||||||
$debug,
|
array_key_exists('debug', $options),
|
||||||
$find_dead_code || $find_references_to !== null,
|
$find_dead_code || $find_references_to !== null,
|
||||||
$find_references_to,
|
$find_references_to,
|
||||||
isset($options['report']) && is_string($options['report']) ? $options['report'] : null
|
isset($options['report']) && is_string($options['report']) ? $options['report'] : null
|
||||||
@ -417,25 +295,6 @@ foreach ($plugins as $plugin_path) {
|
|||||||
Config::getInstance()->addPluginPath($current_dir . DIRECTORY_SEPARATOR . $plugin_path);
|
Config::getInstance()->addPluginPath($current_dir . DIRECTORY_SEPARATOR . $plugin_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options['replace-code'])) {
|
|
||||||
$project_checker->replaceCodeAfterCompletion();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($options['fix-issues'])) {
|
|
||||||
if (!is_string($options['fix-issues']) || !$options['fix-issues']) {
|
|
||||||
die('Expecting a comma-separated string of issues' . PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
$issues = explode(',', $options['fix-issues']);
|
|
||||||
|
|
||||||
$keyed_issues = [];
|
|
||||||
foreach ($issues as $issue) {
|
|
||||||
$keyed_issues[$issue] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$project_checker->fixIssuesAfterCompletion($keyed_issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @psalm-suppress MixedArgument */
|
/** @psalm-suppress MixedArgument */
|
||||||
\Psalm\IssueBuffer::setStartTime(microtime(true));
|
\Psalm\IssueBuffer::setStartTime(microtime(true));
|
||||||
|
|
||||||
|
165
src/psalter.php
Normal file
165
src/psalter.php
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
require_once('command_functions.php');
|
||||||
|
|
||||||
|
use Psalm\Checker\ProjectChecker;
|
||||||
|
use Psalm\Config;
|
||||||
|
|
||||||
|
// show all errors
|
||||||
|
error_reporting(-1);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
ini_set('memory_limit', '2048M');
|
||||||
|
|
||||||
|
// get options from command line
|
||||||
|
$options = getopt(
|
||||||
|
'f:mhr:',
|
||||||
|
[
|
||||||
|
'help', 'debug', 'config:', 'file:', 'root:',
|
||||||
|
'plugin:', 'replace-code', 'issues:', 'target-php-version:', 'dry-run',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (array_key_exists('help', $options)) {
|
||||||
|
$options['h'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('monochrome', $options)) {
|
||||||
|
$options['m'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['config'])) {
|
||||||
|
$options['c'] = $options['config'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['c']) && is_array($options['c'])) {
|
||||||
|
die('Too many config files provided' . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('h', $options)) {
|
||||||
|
echo <<< HELP
|
||||||
|
Usage:
|
||||||
|
psalm [options] [file...]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help
|
||||||
|
Display this help message
|
||||||
|
|
||||||
|
--debug
|
||||||
|
Debug information
|
||||||
|
|
||||||
|
-c, --config=psalm.xml
|
||||||
|
Path to a psalm.xml configuration file. Run psalm --init to create one.
|
||||||
|
|
||||||
|
-m, --monochrome
|
||||||
|
Enable monochrome output
|
||||||
|
|
||||||
|
-r, --root
|
||||||
|
If running Psalm globally you'll need to specify a project root. Defaults to cwd
|
||||||
|
|
||||||
|
--plugin=PATH
|
||||||
|
Executes a plugin, an alternative to using the Psalm config
|
||||||
|
|
||||||
|
--dry-run
|
||||||
|
Shows a diff of all the changes, without making them
|
||||||
|
|
||||||
|
--php-version=PHP_MAJOR_VERSION.PHP_MINOR_VERSION
|
||||||
|
|
||||||
|
--issues=IssueType1,IssueType2
|
||||||
|
If any issues can be fixed automatically, Psalm will update the codebase
|
||||||
|
|
||||||
|
HELP;
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($options['issues'])) {
|
||||||
|
die('Please specify the issues you want to fix with --issues=IssueOne,IssueTwo' . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['root'])) {
|
||||||
|
$options['r'] = $options['root'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_dir = (string)getcwd() . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
if (isset($options['r']) && is_string($options['r'])) {
|
||||||
|
$root_path = realpath($options['r']);
|
||||||
|
|
||||||
|
if (!$root_path) {
|
||||||
|
die('Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_dir = $root_path . DIRECTORY_SEPARATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
requireAutoloaders($current_dir);
|
||||||
|
|
||||||
|
$paths_to_check = getPathsToCheck(isset($options['f']) ? $options['f'] : null);
|
||||||
|
|
||||||
|
$path_to_config = isset($options['c']) && is_string($options['c']) ? realpath($options['c']) : null;
|
||||||
|
|
||||||
|
if ($path_to_config === false) {
|
||||||
|
/** @psalm-suppress InvalidCast */
|
||||||
|
die('Could not resolve path to config ' . (string)$options['c'] . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
$project_checker = new ProjectChecker(
|
||||||
|
new Psalm\Provider\FileProvider(),
|
||||||
|
new Psalm\Provider\ParserCacheProvider(),
|
||||||
|
!array_key_exists('m', $options),
|
||||||
|
false,
|
||||||
|
ProjectChecker::TYPE_CONSOLE,
|
||||||
|
1,
|
||||||
|
array_key_exists('debug', $options)
|
||||||
|
);
|
||||||
|
|
||||||
|
// initialise custom config, if passed
|
||||||
|
if ($path_to_config) {
|
||||||
|
$project_checker->setConfigXML($path_to_config, $current_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $project_checker->getConfig();
|
||||||
|
|
||||||
|
if (!$config) {
|
||||||
|
$project_checker->getConfigForPath($current_dir, $current_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($options['issues']) || !$options['issues']) {
|
||||||
|
die('Expecting a comma-separated list of issues' . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues = explode(',', $options['issues']);
|
||||||
|
|
||||||
|
$keyed_issues = [];
|
||||||
|
foreach ($issues as $issue) {
|
||||||
|
$keyed_issues[$issue] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$php_major_version = PHP_MAJOR_VERSION;
|
||||||
|
$php_minor_version = PHP_MINOR_VERSION;
|
||||||
|
|
||||||
|
if (isset($options['php-version'])) {
|
||||||
|
if (!is_string($options['php-version']) || !preg_match('/^(5\.[456]|7\.[012])^/', $options['php-version'])) {
|
||||||
|
die('Expecting a version number in the format x.y' . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
list($php_major_version, $php_minor_version) = explode('.', $options['php-version']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$project_checker->alterCodeAfterCompletion((int) $php_major_version, (int) $php_minor_version);
|
||||||
|
$project_checker->setIssuesToFix($keyed_issues);
|
||||||
|
|
||||||
|
/** @psalm-suppress MixedArgument */
|
||||||
|
\Psalm\IssueBuffer::setStartTime(microtime(true));
|
||||||
|
|
||||||
|
if ($paths_to_check === null) {
|
||||||
|
$project_checker->check($current_dir);
|
||||||
|
} elseif ($paths_to_check) {
|
||||||
|
foreach ($paths_to_check as $path_to_check) {
|
||||||
|
if (is_dir($path_to_check)) {
|
||||||
|
$project_checker->checkDir($path_to_check);
|
||||||
|
} else {
|
||||||
|
$project_checker->checkFile($path_to_check);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,8 +30,6 @@ class FileManipulationTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->project_checker->setConfig(self::$config);
|
$this->project_checker->setConfig(self::$config);
|
||||||
|
|
||||||
$this->project_checker->add_docblocks = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,10 +37,12 @@ class FileManipulationTest extends TestCase
|
|||||||
*
|
*
|
||||||
* @param string $input_code
|
* @param string $input_code
|
||||||
* @param string $output_code
|
* @param string $output_code
|
||||||
|
* @param string $php_version
|
||||||
|
* @param string[] $issues_to_fix
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testValidCode($input_code, $output_code)
|
public function testValidCode($input_code, $output_code, $php_version, array $issues_to_fix)
|
||||||
{
|
{
|
||||||
$test_name = $this->getName();
|
$test_name = $this->getName();
|
||||||
if (strpos($test_name, 'PHP7-') !== false) {
|
if (strpos($test_name, 'PHP7-') !== false) {
|
||||||
@ -64,9 +64,19 @@ class FileManipulationTest extends TestCase
|
|||||||
$input_code
|
$input_code
|
||||||
);
|
);
|
||||||
|
|
||||||
|
list($php_major_version, $php_minor_version) = explode('.', $php_version);
|
||||||
|
|
||||||
|
$keyed_issues_to_fix = [];
|
||||||
|
|
||||||
|
foreach ($issues_to_fix as $issue) {
|
||||||
|
$keyed_issues_to_fix[$issue] = true;
|
||||||
|
}
|
||||||
|
|
||||||
$file_checker = new FileChecker($file_path, $this->project_checker);
|
$file_checker = new FileChecker($file_path, $this->project_checker);
|
||||||
$this->project_checker->addDocblocksAfterCompletion();
|
|
||||||
$this->project_checker->fixIssuesAfterCompletion(['InvalidReturnType' => true]);
|
$this->project_checker->setIssuesToFix($keyed_issues_to_fix);
|
||||||
|
$this->project_checker->alterCodeAfterCompletion((int) $php_major_version, (int) $php_minor_version);
|
||||||
|
|
||||||
$file_checker->visitAndAnalyzeMethods($context);
|
$file_checker->visitAndAnalyzeMethods($context);
|
||||||
$this->project_checker->updateFile($file_path);
|
$this->project_checker->updateFile($file_path);
|
||||||
$this->assertSame($output_code, $this->project_checker->getFileContents($file_path));
|
$this->assertSame($output_code, $this->project_checker->getFileContents($file_path));
|
||||||
@ -78,7 +88,7 @@ class FileManipulationTest extends TestCase
|
|||||||
public function providerFileCheckerValidCodeParse()
|
public function providerFileCheckerValidCodeParse()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'doesNothing' => [
|
'addMissingVoidReturnType56' => [
|
||||||
'<?php
|
'<?php
|
||||||
function foo() { }',
|
function foo() { }',
|
||||||
'<?php
|
'<?php
|
||||||
@ -86,8 +96,29 @@ class FileManipulationTest extends TestCase
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function foo() { }',
|
function foo() { }',
|
||||||
|
'5.6',
|
||||||
|
['MissingReturnType'],
|
||||||
],
|
],
|
||||||
'returnsString' => [
|
'addMissingVoidReturnType70' => [
|
||||||
|
'<?php
|
||||||
|
function foo() { }',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function foo() { }',
|
||||||
|
'7.0',
|
||||||
|
['MissingReturnType'],
|
||||||
|
],
|
||||||
|
'addMissingVoidReturnType71' => [
|
||||||
|
'<?php
|
||||||
|
function foo() { }',
|
||||||
|
'<?php
|
||||||
|
function foo() : void { }',
|
||||||
|
'7.1',
|
||||||
|
['MissingReturnType'],
|
||||||
|
],
|
||||||
|
'addMissingStringReturnType56' => [
|
||||||
'<?php
|
'<?php
|
||||||
function foo() {
|
function foo() {
|
||||||
return "hello";
|
return "hello";
|
||||||
@ -99,8 +130,98 @@ class FileManipulationTest extends TestCase
|
|||||||
function foo() {
|
function foo() {
|
||||||
return "hello";
|
return "hello";
|
||||||
}',
|
}',
|
||||||
|
'5.6',
|
||||||
|
['MissingReturnType'],
|
||||||
],
|
],
|
||||||
'returnsStringNotInt' => [
|
'addMissingStringReturnType70' => [
|
||||||
|
'<?php
|
||||||
|
function foo() {
|
||||||
|
return "hello";
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
function foo() : string {
|
||||||
|
return "hello";
|
||||||
|
}',
|
||||||
|
'7.0',
|
||||||
|
['MissingReturnType'],
|
||||||
|
],
|
||||||
|
'addMissingNullableStringReturnType56' => [
|
||||||
|
'<?php
|
||||||
|
function foo() {
|
||||||
|
return rand(0, 1) ? "hello" : null;
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
function foo() {
|
||||||
|
return rand(0, 1) ? "hello" : null;
|
||||||
|
}',
|
||||||
|
'5.6',
|
||||||
|
['MissingReturnType'],
|
||||||
|
],
|
||||||
|
'addMissingStringReturnType70' => [
|
||||||
|
'<?php
|
||||||
|
function foo() {
|
||||||
|
return rand(0, 1) ? "hello" : null;
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
function foo() {
|
||||||
|
return rand(0, 1) ? "hello" : null;
|
||||||
|
}',
|
||||||
|
'7.0',
|
||||||
|
['MissingReturnType'],
|
||||||
|
],
|
||||||
|
'addMissingStringReturnType71' => [
|
||||||
|
'<?php
|
||||||
|
function foo() {
|
||||||
|
return rand(0, 1) ? "hello" : null;
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
function foo() : ?string {
|
||||||
|
return rand(0, 1) ? "hello" : null;
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingReturnType'],
|
||||||
|
],
|
||||||
|
'addMissingStringArrayReturnType56' => [
|
||||||
|
'<?php
|
||||||
|
function foo() {
|
||||||
|
return ["hello"];
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*
|
||||||
|
* @psalm-return array{0:string}
|
||||||
|
*/
|
||||||
|
function foo() {
|
||||||
|
return ["hello"];
|
||||||
|
}',
|
||||||
|
'5.6',
|
||||||
|
['MissingReturnType'],
|
||||||
|
],
|
||||||
|
'addMissingStringArrayReturnType70' => [
|
||||||
|
'<?php
|
||||||
|
function foo() {
|
||||||
|
return ["hello"];
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*
|
||||||
|
* @psalm-return array{0:string}
|
||||||
|
*/
|
||||||
|
function foo() : array {
|
||||||
|
return ["hello"];
|
||||||
|
}',
|
||||||
|
'7.0',
|
||||||
|
['MissingReturnType'],
|
||||||
|
],
|
||||||
|
'fixInvalidIntReturnType56' => [
|
||||||
'<?php
|
'<?php
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
@ -115,20 +236,26 @@ class FileManipulationTest extends TestCase
|
|||||||
function foo() {
|
function foo() {
|
||||||
return "hello";
|
return "hello";
|
||||||
}',
|
}',
|
||||||
|
'5.6',
|
||||||
|
['InvalidReturnType'],
|
||||||
],
|
],
|
||||||
'returnStringArray' => [
|
'fixInvalidIntReturnType70' => [
|
||||||
'<?php
|
'<?php
|
||||||
function foo() {
|
/**
|
||||||
return ["hello"];
|
* @return int
|
||||||
|
*/
|
||||||
|
function foo() : int {
|
||||||
|
return "hello";
|
||||||
}',
|
}',
|
||||||
'<?php
|
'<?php
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string
|
||||||
* @psalm-return array{0:string}
|
|
||||||
*/
|
*/
|
||||||
function foo() {
|
function foo() : string {
|
||||||
return ["hello"];
|
return "hello";
|
||||||
}',
|
}',
|
||||||
|
'7.0',
|
||||||
|
['InvalidReturnType'],
|
||||||
],
|
],
|
||||||
'useUnqualifierPlugin' => [
|
'useUnqualifierPlugin' => [
|
||||||
'<?php
|
'<?php
|
||||||
@ -149,6 +276,8 @@ class FileManipulationTest extends TestCase
|
|||||||
|
|
||||||
new D();
|
new D();
|
||||||
}',
|
}',
|
||||||
|
PHP_VERSION,
|
||||||
|
[],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user