diff --git a/dev/tools/phan/config_fixer.php b/dev/tools/phan/config_fixer.php index d5607844f26..841495e2a53 100644 --- a/dev/tools/phan/config_fixer.php +++ b/dev/tools/phan/config_fixer.php @@ -4,6 +4,30 @@ define('DOL_PROJECT_ROOT', __DIR__.'/../../..'); define('DOL_DOCUMENT_ROOT', DOL_PROJECT_ROOT.'/htdocs'); define('PHAN_DIR', __DIR__); + +$DEPRECATED_MODULE_MAPPING = array( + 'actioncomm' => 'agenda', + 'adherent' => 'member', + 'adherent_type' => 'member_type', + 'banque' => 'bank', + 'categorie' => 'category', + 'commande' => 'order', + 'contrat' => 'contract', + 'entrepot' => 'stock', + 'expedition' => 'delivery_note', + 'facture' => 'invoice', + 'ficheinter' => 'intervention', + 'product_fournisseur_price' => 'productsupplierprice', + 'product_price' => 'productprice', + 'projet' => 'project', + 'propale' => 'propal', + 'socpeople' => 'contact', +); + +$deprecatedModuleNameRegex = '/^(?!(?:'.implode('|', array_keys($DEPRECATED_MODULE_MAPPING)).')$).*/'; + +require_once __DIR__.'/plugins/DeprecatedModuleNameFixer.php'; + /** * This configuration will be read and overlaid on top of the * default configuration. Command line arguments will be applied @@ -71,6 +95,8 @@ return [ .'|htdocs/includes/restler/.*' // @phpstan-ignore-line // Included as stub (did not seem properly analysed by phan without it) .'|htdocs/includes/stripe/.*' // @phpstan-ignore-line + //.'|htdocs/[^c][^o][^r][^e][^/].*' // For testing @phpstan-ignore-line + //.'|htdocs/[^h].*' // For testing on restricted set @phpstan-ignore-line .')@', // @phpstan-ignore-line // A list of plugin files to execute. @@ -82,9 +108,14 @@ return [ // // Alternately, you can pass in the full path to a PHP file // with the plugin's implementation (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php') + 'ParamMatchRegexPlugin' => [ + '/^isModEnabled$/' => [0, $deprecatedModuleNameRegex, "DeprecatedModuleName"], + ], 'plugins' => [ + __DIR__.'/plugins/ParamMatchRegexPlugin.php', //'DeprecateAliasPlugin', // __DIR__.'/plugins/NoVarDumpPlugin.php', + __DIR__.'/plugins/GetPostFixerPlugin.php', //'PHPDocToRealTypesPlugin', /* diff --git a/dev/tools/phan/plugins/DeprecatedModuleNameFixer.php b/dev/tools/phan/plugins/DeprecatedModuleNameFixer.php new file mode 100644 index 00000000000..19caf46d95d --- /dev/null +++ b/dev/tools/phan/plugins/DeprecatedModuleNameFixer.php @@ -0,0 +1,126 @@ + + */ + +declare(strict_types=1); + +use Microsoft\PhpParser\Node\Expression\CallExpression; +use Microsoft\PhpParser\Node\QualifiedName; +use Phan\AST\TolerantASTConverter\NodeUtils; +use Phan\CodeBase; +use Phan\IssueInstance; +use Phan\Library\FileCacheEntry; +use Phan\Plugin\Internal\IssueFixingPlugin\FileEdit; +use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet; +use Phan\Plugin\Internal\IssueFixingPlugin\IssueFixer; +use Microsoft\PhpParser\Node\Expression\ArgumentExpression; +use Microsoft\PhpParser\Node\DelimitedList\ArgumentExpressionList; +use Microsoft\PhpParser\Node\StringLiteral; + +/** + * Implements --automatic-fix for GetPostFixerPlugin + * + * This is a prototype, there are various features it does not implement. + */ + +call_user_func(static function (): void { + /** + * @param $code_base @unused-param + * @return ?FileEditSet a representation of the edit to make to replace a call to a function alias with a call to the original function + */ + $fix = static function (CodeBase $code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet { + $DEPRECATED_MODULE_MAPPING = array( + 'actioncomm' => 'agenda', + 'adherent' => 'member', + 'adherent_type' => 'member_type', + 'banque' => 'bank', + 'categorie' => 'category', + 'commande' => 'order', + 'contrat' => 'contract', + 'entrepot' => 'stock', + 'expedition' => 'delivery_note', + 'facture' => 'invoice', + 'ficheinter' => 'intervention', + 'product_fournisseur_price' => 'productsupplierprice', + 'product_price' => 'productprice', + 'projet' => 'project', + 'propale' => 'propal', + 'socpeople' => 'contact', + ); + + $line = $instance->getLine(); + $expected_name = 'isModEnabled'; + $edits = []; + foreach ($contents->getNodesAtLine($line) as $node) { + if (!$node instanceof ArgumentExpressionList) { + continue; + } + $arguments = $node->children; + if (count($arguments) != 1) { + print "Arg Count is ".count($arguments)." - Skip $instance".PHP_EOL; + continue; + } + + $is_actual_call = $node->parent instanceof CallExpression; + if (!$is_actual_call) { + print "Not actual call - Skip $instance".PHP_EOL; + continue; + } + $callable = $node->parent; + + $callableExpression = $callable->callableExpression; + + if ($callableExpression instanceof Microsoft\PhpParser\Node\QualifiedName) { + $actual_name = $callableExpression->getResolvedName(); + } else { + print "Callable expression is ".get_class($callableExpression)."- Skip $instance".PHP_EOL; + continue; + } + + if ((string) $actual_name !== (string) $expected_name) { + print "Name unexpected '$actual_name'!='$expected_name' - Skip $instance".PHP_EOL; + continue; + } + + foreach ($arguments as $i => $argument) { + print "Type$i: ".get_class($argument).PHP_EOL; + } + + $arg1 = $arguments[0]; + + if ($arg1 instanceof ArgumentExpression && $arg1->expression instanceof StringLiteral) { + // Get the string value of the StringLiteral + $stringValue = $arg1->expression->getStringContentsText(); + } else { + print "Expression is not string ".get_class($arg1)."/".get_class($arg1->expression)."- Skip $instance".PHP_EOL; + continue; + } + print "Fixture elem on $line - $actual_name('$stringValue') - $instance".PHP_EOL; + + // Check that module is deprecated + if (isset($DEPRECATED_MODULE_MAPPING[$stringValue])) { + $replacement = $DEPRECATED_MODULE_MAPPING[$stringValue]; + } else { + print "Module is not deprecated in $expected_name - Skip $instance".PHP_EOL; + continue; + } + + // Get the first argument (delimiter) + $moduleargument = $arguments[0]; + + $arg_start_pos = $moduleargument->getStartPosition() + 1; + $arg_end_pos = $moduleargument->getEndPosition() - 1; + + // Remove deprecated module name + $edits[] = new FileEdit($arg_start_pos, $arg_end_pos, $replacement); + } + if ($edits) { + return new FileEditSet($edits); + } + return null; + }; + IssueFixer::registerFixerClosure( + 'DeprecatedModuleName', + $fix + ); +}); diff --git a/dev/tools/phan/plugins/GetPostFixerPlugin.php b/dev/tools/phan/plugins/GetPostFixerPlugin.php new file mode 100644 index 00000000000..4953bf34943 --- /dev/null +++ b/dev/tools/phan/plugins/GetPostFixerPlugin.php @@ -0,0 +1,98 @@ + + */ + +use ast\Node; +use Phan\CodeBase; +use Phan\Language\Context; +use Phan\AST\UnionTypeVisitor; +//use Phan\Language\Element\FunctionInterface; +use Phan\Language\UnionType; +use Phan\Language\Type; +use Phan\PluginV3; +use Phan\PluginV3\AnalyzeFunctionCallCapability; +use Phan\Language\Element\FunctionInterface; +use Phan\Config; + +/* + * 'GetPostFixerPlugin' => [ '\\Foo::bar', '\\Baz::bing' ], + * 'plugins' => [ + * __DIR__.'plugins/GetPostFixerPlugin.php', + * [...] + * ] + */ + +/** + * Prints out call sites of given functions or methods. + */ +final class GetPostFixerPlugin extends PluginV3 implements AnalyzeFunctionCallCapability +{ + /** + * @param CodeBase $code_base Code base + * + * @return array + */ + public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array + { + static $function_call_closures; + + if ($function_call_closures === null) { + $function_call_closures = []; + $self = $this; + $func = 'GETPOST'; + $function_call_closures[$func] + = static function (CodeBase $code_base, Context $context, FunctionInterface $function, array $args, ?Node $node = null) use ($self, $func) { + self::handleCall($code_base, $context, $node, $function, $args, $func, $self); + }; + } + return $function_call_closures; + } + + /** + * @param CodeBase $code_base Code base + * @param Context $context Context + * @param ?Node $node Node + * @param FunctionInterface $function Visited function information + * @param array $args Arguments to the function + * @param string $func_to_analyze Name of the function to analyze (as we defined it) + * @param GetPostFixerPlugin $self This visitor + * + * @return void + */ + private static function handleCall(CodeBase $code_base, Context $context, ?Node $node, FunctionInterface $function, array $args, string $func_to_analyze, $self): void + { + $expr = $args[1] ?? null; + if ($expr === null) { + return; + } + try { + $expr_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr, false); + } catch (Exception $_) { + return; + } + + $expr_value = $expr_type->getRealUnionType()->asValueOrNullOrSelf(); + if (!is_string($expr_value)) { + return; + } + if ($expr_value !== 'int') { + return; + } + + $self->emitIssue( + $code_base, + $context, + 'GetPostShouldBeGetPostInt', + 'Convert {FUNCTION} to {FUNCTION}', + [(string) $function->getFQSEN(), "GETPOSTINT"] + ); + } +} + +if (Config::isIssueFixingPluginEnabled()) { + require_once __DIR__ . '/GetPostFixerPlugin/fixers.php'; +} + +return new GetPostFixerPlugin(); diff --git a/dev/tools/phan/plugins/GetPostFixerPlugin/fixers.php b/dev/tools/phan/plugins/GetPostFixerPlugin/fixers.php new file mode 100644 index 00000000000..662a553eb62 --- /dev/null +++ b/dev/tools/phan/plugins/GetPostFixerPlugin/fixers.php @@ -0,0 +1,123 @@ + + */ + +declare(strict_types=1); + +use Microsoft\PhpParser\Node\Expression\CallExpression; +use Microsoft\PhpParser\Node\QualifiedName; +use Phan\AST\TolerantASTConverter\NodeUtils; +use Phan\CodeBase; +use Phan\IssueInstance; +use Phan\Library\FileCacheEntry; +use Phan\Plugin\Internal\IssueFixingPlugin\FileEdit; +use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet; +use Phan\Plugin\Internal\IssueFixingPlugin\IssueFixer; +use Microsoft\PhpParser\Node\Expression\ArgumentExpression; +use Microsoft\PhpParser\Node\DelimitedList\ArgumentExpressionList; +use Microsoft\PhpParser\Node\StringLiteral; + +/** + * Implements --automatic-fix for GetPostFixerPlugin + * + * This is a prototype, there are various features it does not implement. + */ + +call_user_func(static function (): void { + /** + * @param $code_base @unused-param + * @return ?FileEditSet a representation of the edit to make to replace a call to a function alias with a call to the original function + */ + $fix = static function (CodeBase $code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet { + $line = $instance->getLine(); + $new_name = (string) $instance->getTemplateParameters()[1]; + if ($new_name !== "GETPOSTINT") { + return null; + } + + $function_repr = (string) $instance->getTemplateParameters()[0]; + if (!preg_match('{\\\\(\w+)}', $function_repr, $match)) { + return null; + } + $expected_name = $match[1]; + $edits = []; + foreach ($contents->getNodesAtLine($line) as $node) { + if (!$node instanceof ArgumentExpressionList) { + continue; + } + $arguments = $node->children; + if (count($arguments) != 3) { + print "Arg Count is ".count($arguments)." - Skip $instance".PHP_EOL; + continue; + } + + $is_actual_call = $node->parent instanceof CallExpression; + if (!$is_actual_call) { + print "Not actual call - Skip $instance".PHP_EOL; + continue; + } + $callable = $node->parent; + + $callableExpression = $callable->callableExpression; + + if ($callableExpression instanceof Microsoft\PhpParser\Node\QualifiedName) { + $actual_name = $callableExpression->getResolvedName(); + } else { + print "Callable expression is ".get_class($callableExpression)."- Skip $instance".PHP_EOL; + continue; + } + + if ((string) $actual_name !== (string) $expected_name) { + print "Name unexpected '$actual_name'!='$expected_name' - Skip $instance".PHP_EOL; + continue; + } + + foreach ($arguments as $i => $argument) { + print "Type$i: ".get_class($argument).PHP_EOL; + } + + $arg2 = $arguments[2]; + + if ($arg2 instanceof ArgumentExpression && $arg2->expression instanceof StringLiteral) { + // Get the string value of the StringLiteral + $stringValue = $arg2->expression->getStringContentsText(); + } else { + print "Expression is not string ".get_class($arg2)."/".get_class($arg2->expression)."- Skip $instance".PHP_EOL; + continue; + } + print "Fixture elem on $line - $new_name - $function_repr - arg: $stringValue".PHP_EOL; + + // Get the first argument (delimiter) + $delimiter = $arguments[1]; + // Get the second argument + $secondArgument = $arguments[2]; + + // Get the start position of the delimiter + $arg_start_pos = $delimiter->getStartPosition(); + + // Get the end position of the second argument + $arg_end_pos = $secondArgument->getEndPosition(); + + + + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + $start = $callableExpression->getStartPosition(); + // @phan-suppress-next-line PhanThrowTypeAbsentForCall + $end = $callableExpression->getEndPosition(); + + // Remove second argument + $edits[] = new FileEdit($arg_start_pos, $arg_end_pos, ""); + + // Replace call with GETPOSTINT + $edits[] = new FileEdit($start, $end, (($file_contents[$start] ?? '') === '\\' ? '\\' : '') . $new_name); + } + if ($edits) { + return new FileEditSet($edits); + } + return null; + }; + IssueFixer::registerFixerClosure( + 'GetPostShouldBeGetPostInt', + $fix + ); +});