dolibarr/dev/tools/phan/plugins/ParamMatchRegexPlugin.php
2024-09-06 20:28:06 +08:00

249 lines
6.8 KiB
PHP

<?php
/**
* Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
*
* Phan Plugin to validate that arguments match a regex
*
*
* "ParamMatchRegexPlugin" => [
* "/^test1$/" => [ 0, "/^OK$/"], // Argument 0 must be 'OK'
* "/^test2$/" => [ 1, "/^NOK$/", "Test2Arg1NokError"], // Argument 1 must be 'NOK', error code
* "/^\\MyTest::mymethod$/" => [ 0, "/^NOK$/"], // Argument 0 must be 'NOK'
* ],
* 'plugins' => [
* ".phan/plugins/ParamMatchRegexPlugin.php",
* // [...]
* ],
*/
declare(strict_types=1);
use ast\Node;
use Phan\Config;
use Phan\AST\UnionTypeVisitor;
//use Phan\Language\Element\FunctionInterface;
use Phan\Language\UnionType;
use Phan\Language\Type;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
use Phan\Exception\NodeException;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Exception\FQSENException;
/**
* ParamMatchPlugin hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed
*
* A plugin file must
*
* - Contain a class that inherits from \Phan\PluginV3
*
* - End by returning an instance of that class.
*
* It is assumed without being checked that plugins aren't
* mangling state within the passed code base or context.
*
* Note: When adding new plugins,
* add them to the corresponding section of README.md
*/
class ParamMatchPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return ParamMatchVisitor::class;
}
}
/**
* When __invoke on this class is called with a node, a method
* will be dispatched based on the `kind` of the given node.
*
* Visitors such as this are useful for defining lots of different
* checks on a node based on its kind.
*/
class ParamMatchVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @override
* @param Node $node Node to analyze
*
* @return void
*/
public function visitMethodCall(Node $node): void
{
$method_name = $node->children['method'] ?? null;
if (!\is_string($method_name)) {
return; // Not handled, TODO: handle variable(?) methods
// throw new NodeException($node);
}
try {
// Fetch the list of valid classes, and warn about any undefined classes.
$union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']);
} catch (Exception $_) {
// Phan should already throw for this
return;
}
$class_list = [];
foreach ($union_type->getTypeSet() as $type) {
$class_fqsen = "NoFSQENType";
if ($type instanceof Type) {
try {
$class_fqsen = (string) FullyQualifiedClassName::fromFullyQualifiedString($type->getName());
} catch (FQSENException $_) {
// var_dump([$_, $node]);
continue;
}
} else {
// var_dump( $type) ;
continue;
}
$class_name = (string) $class_fqsen;
$class_list[] = $class_name;
}
/* May need to check list of classes
*/
/*
if (!$class->hasMethodWithName($this->code_base, $method_name, true)) {
throw new NodeException($expr, 'does not have method');
}
$class_name = $class->getName();
*/
foreach ($class_list as $class_name) {
$this->checkRule($node, "$class_name::$method_name");
}
}
/**
* @override
* @param Node $node Node to analyze
*
* @return void
*/
public function visitStaticCall(Node $node): void
{
$class_name = $node->children['class']->children['name'] ?? null;
if (!\is_string($class_name)) {
throw new NodeException($expr, 'does not have class');
}
try {
$class_name = (string) FullyQualifiedClassName::fromFullyQualifiedString($class_name);
} catch (FQSENException $_) {
}
$method_name = $node->children['method'] ?? null;
if (!\is_string($method_name)) {
return;
}
$this->checkRule($node, "$class_name::$method_name");
}
/**
* @override
*
* @param Node $node A node to analyze
*
* @return void
*/
public function visitCall(Node $node): void
{
$name = $node->children['expr']->children['name'] ?? null;
if (!\is_string($name)) {
return;
}
$this->checkRule($node, $name);
}
/**
*
* @param Node $node A node to analyze
* @param string $name function name or fqsn of class::<method>
*
* @return void
*/
public function checkRule(Node $node, string $name)
{
$rules = Config::getValue('ParamMatchRegexPlugin');
foreach ($rules as $regex => $rule) {
if (preg_match($regex, $name)) {
$this->checkParam($node, $rule[0], $rule[1], $name, $rule[2] ?? null);
}
}
}
/**
* Check that argument matches regex at node
*
* @param Node $node Visited node for which to verify arguments match regex
* @param int $argPosition Position of argument to check
* @param string $argRegex Regex to validate against argument
* @param string $functionName Function name for report
* @param string $messageCode Message code to provide in message
*
* @return void
*/
public function checkParam(Node $node, int $argPosition, string $argRegex, $functionName, $messageCode = null): void
{
$args = $node->children['args']->children;
if (!array_key_exists($argPosition, $args)) {
/*
$this->emitPluginIssue(
$this->code_base,
$this->context,
'ParamMatchMissingArgument',
"Argument at %s for %s is missing",
[$argPosition, $function_name]
);
*/
return;
}
$expr = $args[$argPosition];
try {
$expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr, false);
} catch (Exception $_) {
return;
}
$expr_value = $expr_type->getRealUnionType()->asValueOrNullOrSelf();
if (!is_object($expr_value)) {
$list = [(string) $expr_value];
} elseif ($expr_value instanceof UnionType) {
$list = $expr_value->asScalarValues();
} else {
// Note: maybe more types could be supported
return;
}
foreach ($list as $argValue) {
if (!\preg_match($argRegex, (string) $argValue)) {
// Emit an issue if the argument does not match the expected regex pattern
// var_dump([$node,$expr_value,$expr_type->getRealUnionType()]); // Information about node
$this->emitPluginIssue(
$this->code_base,
$this->context,
$messageCode ?? 'ParamMatchRegexError',
"Argument {INDEX} function {FUNCTION} can't have the value {STRING_LITERAL} that does not match the expected pattern '{STRING_LITERAL}'",
[$argPosition, $functionName, json_encode($argValue), $argRegex]
);
}
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new ParamMatchPlugin();