Skip to content

Instantly share code, notes, and snippets.

@beberlei
Last active August 2, 2024 07:09
Show Gist options
  • Save beberlei/7bd3809df1e8edaeb5df5e7adaec40e2 to your computer and use it in GitHub Desktop.
Save beberlei/7bd3809df1e8edaeb5df5e7adaec40e2 to your computer and use it in GitHub Desktop.
<psalm>
<!-- ... -->
<plugins>
<plugin filename="tools/psalm/TidewaysPsalmPlugin.php" />
</plugins>
</psalm>
<?php
// This file is BSD-2 licenses
// Copyright 2020-current Tideways GmbH
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
use Psalm\Plugin\Hook\AfterExpressionAnalysisInterface;
use Psalm\IssueBuffer;
use Psalm\Issue\PluginIssue;
use Psalm\CodeLocation;
class TidewaysPsalmPlugin implements PluginEntryPointInterface, AfterExpressionAnalysisInterface
{
public function __invoke(RegistrationInterface $psalm, ?SimpleXMLElement $config = null) : void
{
}
static public function afterExpressionAnalysis(
PhpParser\Node\Expr $expr,
Psalm\Context $context,
Psalm\StatementsSource $statements_source,
Psalm\Codebase $codebase,
array &$file_replacements = []
) : ?bool
{
if ($expr instanceof PhpParser\Node\Expr\Isset_ &&
$expr->vars[0] instanceof PhpParser\Node\Expr\Variable) {
IssueBuffer::accepts(
new IssetWithNonComplexVariable(
new CodeLocation($statements_source, $expr->vars[0])
),
$statements_source->getSuppressedIssues()
);
}
if ($expr instanceof PHPParser\Node\Expr\Empty_ &&
$expr->expr instanceof PhpParser\Node\Expr\Variable) {
$type = $statements_source->getNodeTypeProvider()->getType($expr->expr);
if ($type !== null && $type->isInt()) {
IssueBuffer::accepts(
new ReplaceEmptyWithMoreSpecificCondition(
'$' . $expr->expr->name . ' === 0',
new CodeLocation($statements_source, $expr)
)
);
} elseif ($type !== null && $type->isString()) {
IssueBuffer::accepts(
new ReplaceEmptyWithMoreSpecificCondition(
'strlen($' . $expr->expr->name . ') === 0',
new CodeLocation($statements_source, $expr)
)
);
} elseif ($type !== null && $type->hasArray()) {
IssueBuffer::accepts(
new ReplaceEmptyWithMoreSpecificCondition(
'count($' . $expr->expr->name . ') === 0',
new CodeLocation($statements_source, $expr)
)
);
} else {
IssueBuffer::accepts(
new ReplaceEmptyWithMoreSpecificCondition(
'no suggestion possible for now',
new CodeLocation($statements_source, $expr)
)
);
}
}
return null;
}
}
class IssetWithNonComplexVariable extends PluginIssue
{
public function __construct(CodeLocation $code_location)
{
parent::__construct('Do not use isset on non-complex variables to test for (non-)emptiness.', $code_location);
}
}
class ReplaceEmptyWithMoreSpecificCondition extends PluginIssue
{
public function __construct(string $replacement, CodeLocation $code_location)
{
parent::__construct('Replace use of empty() with more specific condition: ' . $replacement, $code_location);
}
}
@zerkms
Copy link

zerkms commented Feb 20, 2021

I also could be '$' . $expr->expr->name . " === ''" instead of 'strlen($' . $expr->expr->name . ') === 0'.

@orklah
Copy link

orklah commented Feb 20, 2021

Hi, I'm building a set of psalm plugins to help automatically migrate legacy projects. Do you mind if I publish a fully fleshed plugin based on this idea but with automatic code replacement when it is safe to do so?

@beberlei
Copy link
Author

@orklah yes please you may take the code, I'll update the gist to indicate a BSD-2 license.

@orklah
Copy link

orklah commented Feb 21, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment