From 3c811381aa5e400dc716a67e4f6dae3c447c6696 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Sat, 29 Oct 2016 23:07:13 -0400 Subject: [PATCH] Add an example template checker --- README.md | 15 +++- examples/TemplateChecker.php | 134 +++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 examples/TemplateChecker.php diff --git a/README.md b/README.md index 5c6f95084..96e34fea5 100644 --- a/README.md +++ b/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 + + + + +``` + ### Typing arrays diff --git a/examples/TemplateChecker.php b/examples/TemplateChecker.php new file mode 100644 index 000000000..ac68710ae --- /dev/null +++ b/examples/TemplateChecker.php @@ -0,0 +1,134 @@ +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); + } +}