mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Add an example template checker
This commit is contained in:
parent
98437c52db
commit
3c811381aa
15
README.md
15
README.md
@ -4,7 +4,20 @@ Inspects your code and finds errors
|
||||
|
||||
...
|
||||
|
||||
Because it's based on nikic's PhpParser, it supports @var declarations on lines where etsy/phan does not. Yay.
|
||||
## Checking non-PHP files (e.g. templates)
|
||||
|
||||
Psalm supports the ability to check various PHPish files by extending the `FileChecker` class. For example, if you have a template where the variables are set elsewhere, Psalm can scrape those variables and check the template with those variables pre-populated.
|
||||
|
||||
An example TemplateChecker is provided [here](examples/TemplateChecker.php).
|
||||
|
||||
To ensure your custom `FileChecker` is used, you must update the Psalm `fileExtensions` config in psalm.xml:
|
||||
```xml
|
||||
<fileExtensions>
|
||||
<extension name=".php" />
|
||||
<extension name=".phpt" filetypeHandler="path/to/TemplateChecker.php" />
|
||||
</fileExtensions>
|
||||
```
|
||||
|
||||
|
||||
### Typing arrays
|
||||
|
||||
|
134
examples/TemplateChecker.php
Normal file
134
examples/TemplateChecker.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Example\Template;
|
||||
|
||||
use Psalm;
|
||||
use Psalm\Checker\ClassChecker;
|
||||
use Psalm\Checker\ClassLikeChecker;
|
||||
use Psalm\Checker\FileChecker;
|
||||
use Psalm\Checker\MethodChecker;
|
||||
use Psalm\Context;
|
||||
use Psalm\Checker\StatementsChecker;
|
||||
use Psalm\Checker\CommentChecker;
|
||||
use Psalm\Type;
|
||||
use PhpParser;
|
||||
|
||||
class TemplateChecker extends Psalm\Checker\FileChecker
|
||||
{
|
||||
const THIS_CLASS = 'Psalm\\Example\\Template\\Base';
|
||||
|
||||
public function check($check_classes = true, $check_class_statements = true, Context $file_context = null, $cache = true)
|
||||
{
|
||||
$stmts = $this->getStatements();
|
||||
|
||||
if (empty($stmts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$first_stmt = $stmts[0];
|
||||
|
||||
$this_params = null;
|
||||
|
||||
if (($first_stmt instanceof PhpParser\Node\Stmt\Nop) && ($doc_comment = $first_stmt->getDocComment())) {
|
||||
|
||||
$comment_block = CommentChecker::parseDocComment(trim($doc_comment->getText()));
|
||||
|
||||
if (isset($comment_block['specials']['variablesfrom'])) {
|
||||
$variables_from = trim($comment_block['specials']['variablesfrom'][0]);
|
||||
|
||||
$first_line_regex = '/([A-Za-z\\\0-9]+::[a-z_A-Z]+)?/';
|
||||
|
||||
$matches = [];
|
||||
|
||||
if (!preg_match($first_line_regex, $variables_from, $matches)) {
|
||||
throw new \InvalidArgumentException('Could not interpret doc comment correctly');
|
||||
}
|
||||
|
||||
$this_params = $this->_checkMethod($matches[1]);
|
||||
|
||||
if ($this_params === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this_params->vars_in_scope['$this'] = new Type\Union([new Type\Atomic(self::THIS_CLASS)]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this_params) {
|
||||
$this_params = new Context($this->short_file_name);
|
||||
$this_params->check_variables = false;
|
||||
$this_params->self = self::THIS_CLASS;
|
||||
}
|
||||
|
||||
$this->_checkWithViewClass($this_params);
|
||||
}
|
||||
|
||||
private function _checkMethod($method_id)
|
||||
{
|
||||
$class = explode('::', $method_id)[0];
|
||||
|
||||
if (ClassLikeChecker::checkAbsoluteClassOrInterface($class, $this->short_file_name, 1, []) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this_context = new Context($this->short_file_name);
|
||||
$this_context->self = $class;
|
||||
$this_context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($class)]);
|
||||
|
||||
$constructor_id = $class . '::__construct';
|
||||
|
||||
// this is necessary to enable deep checks
|
||||
ClassChecker::setThisClass($class);
|
||||
|
||||
// check the constructor
|
||||
$constructor_method_checker = ClassChecker::getMethodChecker($constructor_id);
|
||||
|
||||
if ($constructor_method_checker->check($this_context) === false) {
|
||||
ClassChecker::setThisClass(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this_context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($class)]);
|
||||
|
||||
// check the actual method
|
||||
$method_checker = ClassChecker::getMethodChecker($method_id);
|
||||
if ($method_checker->check($this_context) === false) {
|
||||
ClassChecker::setThisClass(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
$view_context = new Context($this->short_file_name);
|
||||
$view_context->self = self::THIS_CLASS;
|
||||
|
||||
// add all $this-> vars to scope
|
||||
foreach ($this_context->vars_possibly_in_scope as $var => $type) {
|
||||
$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;
|
||||
}
|
||||
|
||||
ClassChecker::setThisClass(null);
|
||||
|
||||
return $view_context;
|
||||
}
|
||||
|
||||
protected function _checkWithViewClass(Context $context)
|
||||
{
|
||||
$class_name = self::THIS_CLASS;
|
||||
|
||||
// check that class first
|
||||
FileChecker::getClassLikeCheckerFromClass($class_name)->check(true);
|
||||
|
||||
$stmts = $this->getStatements();
|
||||
|
||||
$class_method = new PhpParser\Node\Stmt\ClassMethod($class_name, ['stmts' => $stmts]);
|
||||
|
||||
$class = new PhpParser\Node\Stmt\Class_($class_name);
|
||||
|
||||
$class_checker = new ClassChecker($class, $this, $class_name);
|
||||
|
||||
(new MethodChecker($class_method, $class_checker))->check($context);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user