2016-10-30 04:07:13 +01:00
|
|
|
<?php
|
2022-12-18 17:15:15 +01:00
|
|
|
|
2017-12-06 07:05:51 +01:00
|
|
|
namespace Psalm\Examples\Template;
|
|
|
|
|
2021-12-27 20:18:33 +01:00
|
|
|
use InvalidArgumentException;
|
2017-12-06 07:05:51 +01:00
|
|
|
use PhpParser;
|
2016-10-30 04:07:13 +01:00
|
|
|
use Psalm;
|
2021-12-13 16:28:14 +01:00
|
|
|
use Psalm\CodeLocation;
|
2018-11-06 03:57:36 +01:00
|
|
|
use Psalm\Codebase;
|
2021-12-13 16:28:14 +01:00
|
|
|
use Psalm\Context;
|
|
|
|
use Psalm\DocComment;
|
2018-11-06 03:57:36 +01:00
|
|
|
use Psalm\Internal\Analyzer\ClassAnalyzer;
|
|
|
|
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
2021-04-30 21:01:33 +02:00
|
|
|
use Psalm\Internal\Analyzer\ClassLikeNameOptions;
|
2018-11-06 03:57:36 +01:00
|
|
|
use Psalm\Internal\Analyzer\MethodAnalyzer;
|
|
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
2022-12-18 17:15:15 +01:00
|
|
|
use Psalm\Internal\MethodIdentifier;
|
2021-12-27 20:18:33 +01:00
|
|
|
use Psalm\Internal\Provider\NodeDataProvider;
|
2021-02-15 22:18:41 +01:00
|
|
|
use Psalm\Node\Stmt\VirtualClass;
|
|
|
|
use Psalm\Node\Stmt\VirtualClassMethod;
|
2020-07-01 15:01:25 +02:00
|
|
|
use Psalm\Storage\MethodStorage;
|
2016-10-30 04:07:13 +01:00
|
|
|
use Psalm\Type;
|
2021-12-13 04:45:57 +01:00
|
|
|
use Psalm\Type\Atomic\TNamedObject;
|
2021-12-13 16:28:14 +01:00
|
|
|
use Psalm\Type\Union;
|
2022-12-18 17:15:15 +01:00
|
|
|
|
|
|
|
use function explode;
|
|
|
|
use function preg_match;
|
|
|
|
use function preg_replace;
|
|
|
|
use function str_replace;
|
|
|
|
use function strtolower;
|
|
|
|
use function trim;
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2023-10-26 17:00:29 +02:00
|
|
|
final class TemplateAnalyzer extends Psalm\Internal\Analyzer\FileAnalyzer
|
2016-10-30 04:07:13 +01:00
|
|
|
{
|
2017-12-06 07:05:51 +01:00
|
|
|
const VIEW_CLASS = 'Your\\View\\Class';
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2020-12-13 07:17:13 +01:00
|
|
|
public function analyze(?Context $file_context = null, ?Context $global_context = null): void
|
2017-12-06 07:05:51 +01:00
|
|
|
{
|
2018-11-11 18:01:14 +01:00
|
|
|
$codebase = $this->project_analyzer->getCodebase();
|
2018-01-21 19:38:51 +01:00
|
|
|
$stmts = $codebase->getStatementsForFile($this->file_path);
|
2017-12-06 07:05:51 +01:00
|
|
|
|
2020-09-20 00:26:51 +02:00
|
|
|
if ($stmts === []) {
|
2017-12-06 07:05:51 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$first_stmt = $stmts[0];
|
|
|
|
|
|
|
|
$this_params = null;
|
|
|
|
|
|
|
|
if (($first_stmt instanceof PhpParser\Node\Stmt\Nop) && ($doc_comment = $first_stmt->getDocComment())) {
|
2020-05-29 04:14:41 +02:00
|
|
|
$comment_block = DocComment::parsePreservingLength($doc_comment);
|
2017-12-06 07:05:51 +01:00
|
|
|
|
2020-05-29 04:14:41 +02:00
|
|
|
if (isset($comment_block->tags['variablesfrom'])) {
|
|
|
|
$variables_from = trim($comment_block->tags['variablesfrom'][0]);
|
2017-12-06 07:05:51 +01:00
|
|
|
|
|
|
|
$first_line_regex = '/([A-Za-z\\\0-9]+::[a-z_A-Z]+)(\s+weak)?/';
|
|
|
|
|
|
|
|
$matches = [];
|
|
|
|
|
|
|
|
if (!preg_match($first_line_regex, $variables_from, $matches)) {
|
2021-12-27 20:18:33 +01:00
|
|
|
throw new InvalidArgumentException('Could not interpret doc comment correctly');
|
2017-12-06 07:05:51 +01:00
|
|
|
}
|
|
|
|
|
2020-10-15 00:51:15 +02:00
|
|
|
/** @psalm-suppress ArgumentTypeCoercion */
|
2021-12-14 01:54:11 +01:00
|
|
|
$method_id = new MethodIdentifier(...explode('::', $matches[1]));
|
2020-02-15 02:54:26 +01:00
|
|
|
|
|
|
|
$this_params = $this->checkMethod($method_id, $first_stmt, $codebase);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
|
|
|
if ($this_params === false) {
|
2017-12-06 07:05:51 +01:00
|
|
|
return;
|
2016-10-30 04:07:13 +01:00
|
|
|
}
|
|
|
|
|
2021-12-13 16:28:14 +01:00
|
|
|
$this_params->vars_in_scope['$this'] = new Union([
|
2021-12-13 04:45:57 +01:00
|
|
|
new TNamedObject(self::VIEW_CLASS),
|
2017-12-06 07:05:51 +01:00
|
|
|
]);
|
2016-10-30 04:07:13 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$this_params) {
|
2017-12-06 07:05:51 +01:00
|
|
|
$this_params = new Context();
|
2016-10-30 04:07:13 +01:00
|
|
|
$this_params->check_variables = false;
|
2017-12-06 07:05:51 +01:00
|
|
|
$this_params->self = self::VIEW_CLASS;
|
2021-12-13 16:28:14 +01:00
|
|
|
$this_params->vars_in_scope['$this'] = new Union([
|
2021-12-13 04:45:57 +01:00
|
|
|
new TNamedObject(self::VIEW_CLASS),
|
2017-12-06 07:05:51 +01:00
|
|
|
]);
|
2016-10-30 04:07:13 +01:00
|
|
|
}
|
|
|
|
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->checkWithViewClass($this_params, $stmts);
|
2016-10-30 04:07:13 +01:00
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2017-12-06 07:05:51 +01:00
|
|
|
* @return Context|false
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2021-12-14 01:54:11 +01:00
|
|
|
private function checkMethod(MethodIdentifier $method_id, PhpParser\Node $stmt, Codebase $codebase)
|
2016-10-30 04:07:13 +01:00
|
|
|
{
|
2018-11-06 03:57:36 +01:00
|
|
|
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
2018-01-02 02:04:03 +01:00
|
|
|
$this,
|
2020-02-15 02:54:26 +01:00
|
|
|
$method_id->fq_class_name,
|
2017-12-06 07:05:51 +01:00
|
|
|
new CodeLocation($this, $stmt),
|
2020-02-11 22:39:33 +01:00
|
|
|
null,
|
2020-03-26 17:35:27 +01:00
|
|
|
null,
|
2017-12-06 07:05:51 +01:00
|
|
|
[],
|
2022-12-18 17:15:15 +01:00
|
|
|
new ClassLikeNameOptions(true),
|
2017-12-06 07:05:51 +01:00
|
|
|
) === false
|
|
|
|
) {
|
2016-10-30 04:07:13 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-12-06 07:05:51 +01:00
|
|
|
$this_context = new Context();
|
2020-02-15 02:54:26 +01:00
|
|
|
$this_context->self = $method_id->fq_class_name;
|
|
|
|
|
|
|
|
$class_storage = $codebase->classlike_storage_provider->get($method_id->fq_class_name);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2021-12-13 16:28:14 +01:00
|
|
|
$this_context->vars_in_scope['$this'] = new Union([new TNamedObject($class_storage->name)]);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2019-03-02 21:07:26 +01:00
|
|
|
$this->project_analyzer->getMethodMutations(
|
2021-12-14 01:54:11 +01:00
|
|
|
new MethodIdentifier($method_id->fq_class_name, '__construct'),
|
2019-03-02 21:07:26 +01:00
|
|
|
$this_context,
|
|
|
|
$this->getRootFilePath(),
|
2022-12-18 17:15:15 +01:00
|
|
|
$this->getRootFileName(),
|
2019-03-02 21:07:26 +01:00
|
|
|
);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2021-12-13 16:28:14 +01:00
|
|
|
$this_context->vars_in_scope['$this'] = new Union([new TNamedObject($class_storage->name)]);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
|
|
|
// check the actual method
|
2019-03-02 21:07:26 +01:00
|
|
|
$this->project_analyzer->getMethodMutations(
|
|
|
|
$method_id,
|
|
|
|
$this_context,
|
|
|
|
$this->getRootFilePath(),
|
2022-12-18 17:15:15 +01:00
|
|
|
$this->getRootFileName(),
|
2019-03-02 21:07:26 +01:00
|
|
|
);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2017-12-06 07:05:51 +01:00
|
|
|
$view_context = new Context();
|
2020-02-15 02:54:26 +01:00
|
|
|
$view_context->self = strtolower(self::VIEW_CLASS);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
|
|
|
// add all $this-> vars to scope
|
2018-01-28 18:01:51 +01:00
|
|
|
foreach ($this_context->vars_possibly_in_scope as $var => $_) {
|
2016-10-30 04:07:13 +01:00
|
|
|
$view_context->vars_in_scope[str_replace('$this->', '$', $var)] = Type::getMixed();
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($this_context->vars_in_scope as $var => $type) {
|
|
|
|
$view_context->vars_in_scope[str_replace('$this->', '$', $var)] = $type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $view_context;
|
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2018-01-21 18:44:46 +01:00
|
|
|
* @param array<PhpParser\Node\Stmt> $stmts
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
protected function checkWithViewClass(Context $context, array $stmts): void
|
2016-10-30 04:07:13 +01:00
|
|
|
{
|
2017-12-06 07:05:51 +01:00
|
|
|
$pseudo_method_stmts = [];
|
2016-11-21 21:51:38 +01:00
|
|
|
|
2017-12-06 07:05:51 +01:00
|
|
|
foreach ($stmts as $stmt) {
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Use_) {
|
|
|
|
$this->visitUse($stmt);
|
|
|
|
} else {
|
|
|
|
$pseudo_method_stmts[] = $stmt;
|
|
|
|
}
|
2016-11-21 21:51:38 +01:00
|
|
|
}
|
|
|
|
|
2017-12-06 07:05:51 +01:00
|
|
|
$pseudo_method_name = preg_replace('/[^a-zA-Z0-9_]+/', '_', $this->file_name);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2021-02-15 22:18:41 +01:00
|
|
|
$class_method = new VirtualClassMethod($pseudo_method_name, ['stmts' => []]);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2021-02-15 22:18:41 +01:00
|
|
|
$class = new VirtualClass(self::VIEW_CLASS);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
$class_analyzer = new ClassAnalyzer($class, $this, self::VIEW_CLASS);
|
2017-12-06 07:05:51 +01:00
|
|
|
|
2020-07-01 15:01:25 +02:00
|
|
|
$view_method_analyzer = new MethodAnalyzer($class_method, $class_analyzer, new MethodStorage());
|
2017-12-06 07:05:51 +01:00
|
|
|
|
|
|
|
if (!$context->check_variables) {
|
2018-11-11 18:01:14 +01:00
|
|
|
$view_method_analyzer->addSuppressedIssue('UndefinedVariable');
|
2017-12-06 07:05:51 +01:00
|
|
|
}
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
$statements_source = new StatementsAnalyzer(
|
|
|
|
$view_method_analyzer,
|
2022-12-18 17:15:15 +01:00
|
|
|
new NodeDataProvider(),
|
2019-11-25 17:44:54 +01:00
|
|
|
);
|
2016-10-30 04:07:13 +01:00
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
$statements_source->analyze($pseudo_method_stmts, $context);
|
2016-10-30 04:07:13 +01:00
|
|
|
}
|
|
|
|
}
|