diff --git a/lib/ClassChecker.php b/lib/ClassChecker.php new file mode 100644 index 000000000..79fbe2fce --- /dev/null +++ b/lib/ClassChecker.php @@ -0,0 +1,97 @@ +_class = $class; + $this->_namespace = $namespace; + $this->_aliased_classes = $aliased_classes; + $this->_file_name = $file_name; + + self::$_existing_classes[self::getAbsoluteClass($class->name, $this->_namespace, [])] = 1; + } + + public function check() + { + if ($this->_class->extends instanceof PhpParser\Node\Name) { + self::checkClassName($this->_class->extends, $this->_namespace, $this->_aliased_classes, $this->_file_name); + } + + foreach ($this->_class->stmts as $stmt) { + if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) { + $method_checker = new ClassMethodChecker($stmt, $this->_namespace, $this->_aliased_classes, $this->_file_name, $this->_class->name, $this->_class->extends); + $method_checker->check(); + } + } + } + + public static function checkClassName(PhpParser\Node\Name $class_name, $namespace, array $aliased_classes, $file_name) + { + if ($class_name->parts[0] === 'static') { + return; + } + + $absolute_class = self::getAbsoluteClassFromName($class_name, $namespace, $aliased_classes); + + if (!isset(self::$_existing_classes[$absolute_class]) && !class_exists($absolute_class, true) && !interface_exists($absolute_class, true)) { + throw new CodeException('Class ' . $absolute_class . ' does not exist', $file_name, $class_name->getLine()); + } + + self::$_existing_classes[$absolute_class] = 1; + } + + public static function getAbsoluteClassFromName(PhpParser\Node\Name $class_name, $namespace, array $aliased_classes) + { + if ($class_name instanceof PhpParser\Node\Name\FullyQualified) { + return '\\' . implode('\\', $class_name->parts); + } + + return self::getAbsoluteClass(implode('\\', $class_name->parts), $namespace, $aliased_classes); + } + + public static function getAbsoluteClass($class, $namespace, array $imported_namespaces) { + if ($class[0] === '\\') { + return $class; + } + + if (strpos($class, '\\') !== false) { + $class_parts = explode('\\', $class); + $first_namespace = array_shift($class_parts); + + if (isset($imported_namespaces[$first_namespace])) { + return self::_addSlash($imported_namespaces[$first_namespace] . '\\' . implode('\\', $class_parts)); + } + } + else if (isset($imported_namespaces[$class])) { + return self::_addSlash($imported_namespaces[$class]); + } + + if ($namespace && substr($namespace, -1) !== '\\') { + $namespace .= '\\'; + } + + return self::_addSlash($namespace . $class); + } + + protected static function _addSlash($class) + { + if ($class[0] === '\\') { + return $class; + } + + return '\\' . $class; + } +} diff --git a/lib/ClassMethodChecker.php b/lib/ClassMethodChecker.php new file mode 100644 index 000000000..803609d95 --- /dev/null +++ b/lib/ClassMethodChecker.php @@ -0,0 +1,9 @@ +_function->uses as $use) { + $this->_declared_variables[$use->var] = 1; + } + + parent::check(); + } +} diff --git a/lib/CodeException.php b/lib/CodeException.php new file mode 100644 index 000000000..5a6e0a8b0 --- /dev/null +++ b/lib/CodeException.php @@ -0,0 +1,22 @@ +line_number = $line_number; + $this->file_name = $file_name; + + parent::__construct($message, $code, $previous); + } + + public function getSourceLine() + { + return $this->line_number; + } +} diff --git a/lib/FileChecker.php b/lib/FileChecker.php new file mode 100644 index 000000000..fee720597 --- /dev/null +++ b/lib/FileChecker.php @@ -0,0 +1,100 @@ +_file_name = $file_name; + } + + public function check($check_classes = true) + { + $contents = file_get_contents($this->_file_name); + + $cache = \Application::getCache(); + $cache_key = 'parser0' . md5($contents); + + $stmts = $cache->get($cache_key); + + if (!$stmts) { + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + + $stmts = $parser->parse($contents); + + $cache->set($cache_key, $stmts); + } + + foreach ($stmts as $stmt) { + if ($stmt instanceof PhpParser\Node\Stmt\Class_) { + if ($check_classes) { + $this->_checkClass($stmt, ''); + } + } + else if ($stmt instanceof PhpParser\Node\Stmt\Namespace_) { + $this->_checkNamespace($stmt, $check_classes); + } + else if ($stmt instanceof PhpParser\Node\Stmt\Use_) { + foreach ($stmt->uses as $use) { + $this->_aliased_classes[$use->alias] = implode('\\', $use->name->parts); + } + } + } + + self::$_file_checkers[$this->_file_name] = $this; + } + + public function _checkNamespace(PhpParser\Node\Stmt\Namespace_ $namespace, $check_classes) + { + foreach ($namespace->stmts as $stmt) { + if ($stmt instanceof PhpParser\Node\Stmt\Class_) { + if ($namespace->name === null) { + throw new CodeException('Empty namespace', $this->_file_name, $stmt->getLine()); + } + + $this->_namespace = implode('\\', $namespace->name->parts); + + if ($check_classes) { + $this->_checkClass($stmt, $this->_namespace, $this->_aliased_classes); + } + } + else if ($stmt instanceof PhpParser\Node\Stmt\Use_) { + foreach ($stmt->uses as $use) { + $this->_aliased_classes[$use->alias] = implode('\\', $use->name->parts); + } + } + } + } + + public function _checkClass(PhpParser\Node\Stmt\Class_ $class, $namespace = null) + { + (new ClassChecker($class, $namespace, $this->_aliased_classes, $this->_file_name))->check(); + } + + public function getAbsoluteClass($class) + { + return ClassChecker::getAbsoluteClass($class, $this->_namespace, $this->_aliased_classes); + } + + public static function getAbsoluteClassInFile($class, $file_name) + { + if (isset(self::$_file_checkers[$file_name])) { + return self::$_file_checkers[$file_name]->getAbsoluteClass($class); + } + + $file_checker = new FileChecker($file_name); + $file_checker->check(false); + return $file_checker->getAbsoluteClass($class); + } +} diff --git a/lib/FunctionChecker.php b/lib/FunctionChecker.php new file mode 100644 index 000000000..100c30adc --- /dev/null +++ b/lib/FunctionChecker.php @@ -0,0 +1,1114 @@ +_function = $function; + $this->_aliased_classes = $aliased_classes; + $this->_namespace = $namespace; + $this->_class_name = $class_name; + $this->_class_extends = $class_extends; + $this->_file_name = $file_name; + + $this->_absolute_class = ClassChecker::getAbsoluteClass($this->_class_name, $this->_namespace, []); + + if ($function instanceof PhpParser\Node\Stmt\ClassMethod) { + self::_registerMethod($function); + } + } + + public function check() + { + foreach ($this->_function->params as $param) { + if ($param->type) { + if (is_object($param->type)) { + if (!in_array($param->type->parts[0], ['self', 'parent']) && $this->_check_classes) { + ClassChecker::checkClassName($param->type, $this->_namespace, $this->_aliased_classes, $this->_file_name); + } + } + } + + $this->_declared_variables[$param->name] = 1; + + if ($param->type && is_object($param->type)) { + $this->_known_types[$param->name] = + $param->type->parts === ['self'] ? + $this->_absolute_class : + ClassChecker::getAbsoluteClassFromName($param->type, $this->_namespace, $this->_aliased_classes); + } + } + + $types_in_scope = []; + if ($this->_function->stmts) { + $this->_checkStatements($this->_function->stmts, $types_in_scope); + } + } + + protected function _checkStatements(array $stmts, array &$types_in_scope) + { + $has_returned = false; + + foreach ($stmts as $stmt) { + + if ($has_returned) { + throw new CodeException('Expressions after return', $this->_file_name, $stmt->getLine()); + } + + if ($stmt instanceof PhpParser\Node\Stmt\If_) { + $this->_checkIf($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) { + $this->_checkTryCatch($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\For_) { + $this->_checkFor($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\Foreach_) { + $this->_checkForeach($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\While_) { + $this->_checkWhile($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\Do_) { + $this->_checkDo($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\Unset_) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Stmt\Return_) { + $has_returned = true; + $this->_checkReturn($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\Throw_) { + $this->_checkThrow($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\Switch_) { + $this->_checkSwitch($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Stmt\Break_) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Stmt\Continue_) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Stmt\Static_) { + foreach ($stmt->vars as $var) { + if ($var instanceof PhpParser\Node\Stmt\StaticVar) { + if (is_string($var->name)) { + if ($this->_check_variables) { + $this->_declared_variables[$var->name] = 1; + } + } + else { + $this->_checkExpression($var->name, $types_in_scope); + } + + if ($var->default) { + $this->_checkExpression($var->default, $types_in_scope); + } + } + else { + $this->_checkExpression($var, $types_in_scope); + } + } + } + else if ($stmt instanceof PhpParser\Node\Stmt\Echo_) { + foreach ($stmt->exprs as $expr) { + $this->_checkExpression($expr, $types_in_scope); + } + } + else if ($stmt instanceof PhpParser\Node\Stmt\Function_) { + $function_checker = new FunctionChecker($stmt, $this->_namespace, $this->_aliased_classes, $this->_file_name, $this->_class_name, $this->_class_extends); + $function_checker->check(); + } + else if ($stmt instanceof PhpParser\Node\Expr) { + $this->_checkExpression($stmt, $types_in_scope); + } + else { + Application::var_dump('Unrecognised statement'); + Application::var_dump($stmt); + } + } + } + + protected function _checkIf(PhpParser\Node\Stmt\If_ $stmt, array &$types_in_scope) + { + $this->_checkCondition($stmt->cond, $types_in_scope); + + $instanceof_class = null; + + $if_types = []; + + if ($stmt->cond instanceof PhpParser\Node\Expr\Instanceof_) { + $if_types = $this->_getInstanceOfTypes($stmt->cond); + } + + $this->_checkStatements($stmt->stmts, array_merge($types_in_scope, $if_types)); + + foreach ($stmt->elseifs as $elseif) { + $this->_checkElseIf($elseif, $types_in_scope); + } + + if ($stmt->else) { + $this->_checkElse($stmt->else, $types_in_scope); + } + } + + protected function _checkElseIf(PhpParser\Node\Stmt\ElseIf_ $stmt, array &$types_in_scope) + { + $this->_checkCondition($stmt->cond, $types_in_scope); + + $if_types = []; + + if ($stmt->cond instanceof PhpParser\Node\Expr\Instanceof_) { + $if_types = $this->_getInstanceOfTypes($stmt->cond); + } + + $this->_checkStatements($stmt->stmts, array_merge($types_in_scope, $if_types)); + } + + protected function _checkElse(PhpParser\Node\Stmt\Else_ $stmt, array &$types_in_scope) + { + $this->_checkStatements($stmt->stmts, $types_in_scope); + } + + protected function _checkCondition(PhpParser\Node\Expr $stmt, array &$types_in_scope) + { + $this->_checkExpression($stmt, $types_in_scope); + } + + protected function _getInstanceOfTypes(PhpParser\Node\Expr $stmt) + { + $if_types = []; + + if ($stmt->expr instanceof PhpParser\Node\Expr\Variable && is_string($stmt->expr->name) && $stmt->class instanceof PhpParser\Node\Name) { + if (!in_array($stmt->class->parts[0], ['self', 'static', 'parent'])) { + $instanceof_class = ClassChecker::getAbsoluteClassFromName($stmt->class, $this->_namespace, $this->_aliased_classes); + $if_types[$stmt->expr->name] = $instanceof_class; + } + else if ($stmt->class->parts === ['self']) { + $if_types[$stmt->expr->name] = $this->_absolute_class; + } + } + + return $if_types; + } + + protected function _checkExpression(PhpParser\Node\Expr $stmt, array &$types_in_scope = []) + { + if ($stmt instanceof PhpParser\Node\Expr\Variable) { + $this->_checkVariable($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Assign) { + $this->_checkAssignment($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\AssignOp) { + $this->_checkAssignmentOperation($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\MethodCall) { + $this->_checkMethodCall($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\StaticCall) { + $this->_checkStaticCall($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { + if (preg_match('/^[A-Z_0-9]+$/', $stmt->name->parts[0])) { + $this->_checkConstFetch($stmt); + } + } + else if ($stmt instanceof PhpParser\Node\Scalar\String_) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Scalar\EncapsedStringPart) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Scalar\MagicConst) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Scalar\LNumber) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Scalar\DNumber) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Expr\UnaryMinus) { + $this->_checkExpression($stmt->expr); + } + else if ($stmt instanceof PhpParser\Node\Expr\Isset_) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Expr\ClassConstFetch) { + $this->_checkClassConstFetch($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\PropertyFetch) { + $this->_checkPropertyFetch($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\StaticPropertyFetch) { + $this->_checkStaticPropertyFetch($stmt); + } + else if ($stmt instanceof PhpParser\Node\Expr\BitwiseNot) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) { + $this->_checkExpression($stmt->left, $types_in_scope); + $this->_checkExpression($stmt->right, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\PostInc) { + $this->_checkExpression($stmt->var); + } + else if ($stmt instanceof PhpParser\Node\Expr\PostDec) { + $this->_checkExpression($stmt->var); + } + else if ($stmt instanceof PhpParser\Node\Expr\PreInc) { + $this->_checkExpression($stmt->var); + } + else if ($stmt instanceof PhpParser\Node\Expr\PreDec) { + $this->_checkExpression($stmt->var); + } + else if ($stmt instanceof PhpParser\Node\Expr\New_) { + $this->_checkNew($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Array_) { + $this->_checkArray($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Scalar\Encapsed) { + $this->_checkEncapsulatedString($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\FuncCall) { + $this->_checkFunctionCall($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Ternary) { + $this->_checkTernary($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\BooleanNot) { + $this->_checkBooleanNot($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Empty_) { + $this->_checkEmpty($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Closure) { + $closure_checker = new ClosureChecker($stmt, $this->_namespace, $this->_aliased_classes, $this->_file_name, $this->_class_name, $this->_class_extends); + $closure_checker->check(); + } + else if ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) { + $this->_checkArrayAccess($stmt, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Cast\Int_) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Cast\Double) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Cast\Bool_) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Cast\String_) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Cast\Object_) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Cast\Array_) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Clone_) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\Instanceof_) { + $this->_checkExpression($stmt->expr, $types_in_scope); + + if ($stmt->class instanceof PhpParser\Node\Name && !in_array($stmt->class->parts[0], ['self', 'static', 'parent'])) { + if ($this->_check_classes) { + ClassChecker::checkClassName($stmt->class, $this->_namespace, $this->_aliased_classes, $this->_file_name); + } + } + } + else if ($stmt instanceof PhpParser\Node\Expr\Exit_) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Expr\Include_) { + $this->_checkExpression($stmt->expr); + $this->_check_classes = false; + $this->_check_variables = false; + } + else if ($stmt instanceof PhpParser\Node\Expr\Eval_) { + $this->_checkExpression($stmt->expr); + $this->_check_classes = false; + $this->_check_variables = false; + } + else if ($stmt instanceof PhpParser\Node\Expr\AssignRef) { + if ($stmt->var instanceof PhpParser\Node\Expr\Variable) { + $this->_declared_variables[$stmt->var->name] = 1; + } + else { + $this->_checkExpression($stmt->var, $types_in_scope); + } + + $this->_checkExpression($stmt->expr, $types_in_scope); + } + else if ($stmt instanceof PhpParser\Node\Expr\ErrorSuppress) { + // do nothing + } + else if ($stmt instanceof PhpParser\Node\Expr\ShellExec) { + throw new CodeException('Use of shell_exec', $this->_file_name, $stmt->getLine()); + } + else { + Application::var_dump('Unrecognised expression'); + Application::var_dump($stmt); + } + } + + protected function _checkVariable(PhpParser\Node\Expr\Variable $stmt, array &$types_in_scope, $method_id = null, $argument_offset = -1) + { + if (!$this->_check_variables) { + return; + } + + if (in_array($stmt->name, ['this', '_SERVER', '_GET', '_POST', '_COOKIE', '_REQUEST', '_FILES', '_ENV', 'GLOBALS'])) { + return; + } + + if (!is_string($stmt->name)) { + $this->_checkExpression($stmt->name); + return; + } + + if (!array_key_exists($stmt->name, $this->_declared_variables)) { + if ($method_id) { + if (strpos($method_id, '::') !== false) { + if (self::_isPassedByRef($method_id, $argument_offset)) { + $this->_declared_variables[$stmt->name] = 1; + return; + } + } + else { + $reflection_parameters = (new \ReflectionFunction($method_id))->getParameters(); + + // if value is passed by reference + if ($argument_offset < count($reflection_parameters) && $reflection_parameters[$argument_offset]->isPassedByReference()) { + $this->_declared_variables[$stmt->name] = 1; + return; + } + } + } + + throw new CodeException('Cannot find referenced variable ' . $stmt->name, $this->_file_name, $stmt->getLine()); + } + else { + if (isset($this->_known_types[$stmt->name])) { + $stmt->returnType = $this->_known_types[$stmt->name]; + } + + if (isset($types_in_scope[$stmt->name])) { + $stmt->returnType = $types_in_scope[$stmt->name]; + } + } + } + + protected function _checkPropertyFetch(PhpParser\Node\Expr\PropertyFetch $stmt, array &$types_in_scope) + { + if ($stmt->var instanceof PhpParser\Node\Expr\Variable) { + if ($stmt->var->name === 'this') { + + } + else { + $this->_checkVariable($stmt->var, $types_in_scope); + } + } + else { + $this->_checkExpression($stmt->var, $types_in_scope); + } + } + + protected function _checkNew(PhpParser\Node\Expr\New_ $stmt, array &$types_in_scope) + { + if ($stmt->class instanceof PhpParser\Node\Name && !in_array($stmt->class->parts[0], ['self', 'static', 'parent'])) { + if ($this->_check_classes) { + ClassChecker::checkClassName($stmt->class, $this->_namespace, $this->_aliased_classes, $this->_file_name); + $stmt->returnType = ClassChecker::getAbsoluteClassFromName($stmt->class, $this->_namespace, $this->_aliased_classes); + } + } + + foreach ($stmt->args as $arg) { + $this->_checkExpression($arg->value, $types_in_scope); + } + + + } + + protected function _checkArray(PhpParser\Node\Expr\Array_ $stmt, array &$types_in_scope) + { + foreach ($stmt->items as $item) { + if ($item->key) { + $this->_checkExpression($item->key, $types_in_scope); + } + + $this->_checkExpression($item->value, $types_in_scope); + } + } + + protected function _checkTryCatch(PhpParser\Node\Stmt\TryCatch $stmt, array &$types_in_scope) + { + $this->_checkStatements($stmt->stmts, $types_in_scope); + + foreach ($stmt->catches as $catch) { + $this->_declared_variables[$catch->var] = 1; + $this->_known_types[$catch->var] = ClassChecker::getAbsoluteClassFromName($catch->type, $this->_namespace, $this->_aliased_classes); + + if ($this->_check_classes) { + ClassChecker::checkClassName($catch->type, $this->_namespace, $this->_aliased_classes, $this->_file_name); + } + + $this->_checkStatements($catch->stmts, $types_in_scope); + } + + if ($stmt->finallyStmts) { + $this->_checkStatements($stmt->finallyStmts, $types_in_scope); + } + } + + protected function _checkFor(PhpParser\Node\Stmt\For_ $stmt, array &$types_in_scope) + { + foreach ($stmt->init as $init) { + $this->_checkExpression($init, $types_in_scope); + } + + foreach ($stmt->cond as $condition) { + $this->_checkCondition($init, $types_in_scope); + } + + foreach ($stmt->loop as $expr) { + $this->_checkExpression($expr); + } + + $this->_checkStatements($stmt->stmts, $types_in_scope); + } + + protected function _checkForeach(PhpParser\Node\Stmt\Foreach_ $stmt, array &$types_in_scope) + { + $this->_checkExpression($stmt->expr, $types_in_scope); + + if ($stmt->keyVar) { + $this->_declared_variables[$stmt->keyVar->name] = 1; + } + + if ($stmt->valueVar) { + $this->_declared_variables[$stmt->valueVar->name] = 1; + } + + $this->_checkStatements($stmt->stmts, $types_in_scope); + } + + protected function _checkWhile(PhpParser\Node\Stmt\While_ $stmt, array &$types_in_scope) + { + $this->_checkCondition($stmt->cond, $types_in_scope); + + $this->_checkStatements($stmt->stmts, $types_in_scope); + } + + protected function _checkDo(PhpParser\Node\Stmt\Do_ $stmt, array &$types_in_scope) + { + $this->_checkStatements($stmt->stmts, $types_in_scope); + + $this->_checkCondition($stmt->cond, $types_in_scope); + } + + protected function _checkAssignment(PhpParser\Node\Expr\Assign $stmt, array &$types_in_scope) + { + if ($stmt->var instanceof PhpParser\Node\Expr\Variable && is_string($stmt->var->name)) { + $this->_declared_variables[$stmt->var->name] = 1; + } + else if ($stmt->var instanceof PhpParser\Node\Expr\List_) { + foreach ($stmt->var->vars as $var) { + if ($var) { + $this->_declared_variables[$var->name] = 1; + } + } + } + // if it's an array assignment + else if ($stmt->var instanceof PhpParser\Node\Expr\ArrayDimFetch && $stmt->var->var instanceof PhpParser\Node\Expr\Variable) { + $this->_declared_variables[$stmt->var->var->name] = 1; + } + + $this->_checkExpression($stmt->expr, $types_in_scope); + + if ($stmt->var instanceof PhpParser\Node\Expr\Variable && is_string($stmt->var->name)) { + $comments = []; + $doc_comment = $stmt->getDocComment(); + + if ($doc_comment) { + $comments = self::_parseDocComment($doc_comment); + } + + if ($comments && isset($comments['specials']['var'][0])) { + $type = explode(' ', $comments['specials']['var'][0])[0]; + + if ($type[0] === strtoupper($type[0])) { + $this->_known_types[$stmt->var->name] = ClassChecker::getAbsoluteClass($type, $this->_namespace, $this->_aliased_classes); + } + } + else if (isset($stmt->expr->returnType)) { + $var_name = $stmt->var->name; + + if (isset($this->_known_types[$var_name])) { + $existing_type = $this->_known_types[$var_name]; + + if ($existing_type !== 'mixed') { + if (is_a($existing_type, $stmt->expr->returnType, true)) { + // downcast + $this->_known_types[$var_name] = $stmt->expr->returnType; + } + else if (is_a($stmt->expr->returnType, $existing_type, true)) { + if (!isset($types_in_scope[$var_name])) { + $types_in_scope[$var_name] = $stmt->expr->returnType; + } + } + else { + $this->_known_types[$stmt->var->name] = 'mixed'; + } + } + } + else { + $this->_known_types[$stmt->var->name] = $stmt->expr->returnType; + } + } + } + } + + protected function _checkAssignmentOperation(PhpParser\Node\Expr\AssignOp $stmt, array &$types_in_scope) + { + $this->_checkExpression($stmt->var, $types_in_scope); + $this->_checkExpression($stmt->expr, $types_in_scope); + } + + protected function _checkMethodCall(PhpParser\Node\Expr\MethodCall $stmt, array &$types_in_scope) + { + $this->_checkExpression($stmt->var, $types_in_scope); + + $absolute_class = null; + $method_id = null; + + if ($stmt->var instanceof PhpParser\Node\Expr\Variable) { + if ($stmt->var->name === 'this') { + if (!$this->_class_name) { + throw new CodeException('Use of $this in non-class context', $this->_file_name, $stmt->getLine()); + } + + $absolute_class = $this->_absolute_class; + } + else if (!is_string($stmt->var->name)) { + $this->_checkExpression($stmt->var->name, $types_in_scope); + } + else if (isset($this->_known_types[$stmt->var->name])) { + if (isset($types_in_scope[$stmt->var->name])) { + $absolute_class = $types_in_scope[$stmt->var->name]; + } + else { + $absolute_class = $this->_known_types[$stmt->var->name]; + } + } + } + else if ($stmt->var instanceof PhpParser\Node\Expr) { + $this->_checkExpression($stmt->var, $types_in_scope); + } + + if (!$absolute_class && isset($stmt->var->returnType)) { + $absolute_class = $stmt->var->returnType; + } + + if ($absolute_class && $absolute_class[0] === strtoupper($absolute_class[0]) && $this->_check_methods && is_string($stmt->name) && !method_exists($absolute_class, '__call')) { + $method_id = $absolute_class . '::' . $stmt->name; + + if (!self::_methodExists($method_id)) { + throw new CodeException('Method ' . $method_id . ' does not exist', $this->_file_name, $stmt->getLine()); + } + + $return_types = $this->_getMethodReturnTypes($method_id); + + if ($return_types) { + // @todo should work for multiple types + $return_type = $return_types[0]; + + $stmt->returnType = $return_type; + } + } + + $this->_checkMethodParams($stmt->args, $method_id, $types_in_scope); + } + + protected function _checkStaticCall(PhpParser\Node\Expr\StaticCall $stmt, array &$types_in_scope) + { + if ($stmt->class instanceof PhpParser\Node\Expr\Variable || $stmt->class instanceof PhpParser\Node\Expr\ArrayDimFetch) { + // this is when calling $some_class::staticMethod() - which is a shitty way of doing things + // because it can't be statically type-checked + return; + } + + $method_id = null; + $absolute_class = null; + + if (count($stmt->class->parts) === 1 && in_array($stmt->class->parts[0], ['self', 'static', 'parent'])) { + if ($stmt->class->parts[0] === 'parent') { + $absolute_class = ClassChecker::getAbsoluteClassFromName($this->_class_extends, $this->_namespace, $this->_aliased_classes); + } + else { + $absolute_class = ($this->_namespace ? '\\' : '') . $this->_namespace . '\\' . $this->_class_name; + } + } + else if ($this->_check_classes) { + ClassChecker::checkClassName($stmt->class, $this->_namespace, $this->_aliased_classes, $this->_file_name); + $absolute_class = ClassChecker::getAbsoluteClassFromName($stmt->class, $this->_namespace, $this->_aliased_classes); + } + + if ($absolute_class && $this->_check_methods && is_string($stmt->name) && !method_exists($absolute_class, '__callStatic')) { + $method_id = $absolute_class . '::' . $stmt->name; + + if (!self::_methodExists($method_id)) { + throw new CodeException('Method ' . $method_id . ' does not exist', $this->_file_name, $stmt->getLine()); + } + + $return_types = $this->_getMethodReturnTypes($method_id); + + if ($return_types) { + // @todo should work for multiple types + $return_type = $return_types[0]; + + $stmt->returnType = $return_type; + } + } + + $this->_checkMethodParams($stmt->args, $method_id, $types_in_scope); + } + + protected function _checkMethodParams(array $args, $method_id, array &$types_in_scope) + { + foreach ($args as $i => $arg) { + if ($arg->value instanceof PhpParser\Node\Expr\Variable) { + if ($method_id) { + $this->_checkVariable($arg->value, $types_in_scope, $method_id, $i); + } + else if (is_string($arg->value->name)) { + // we don't know if it exists, assume it's passed by reference + $this->_declared_variables[$arg->value->name] = 1; + } + } + else { + $this->_checkExpression($arg->value, $types_in_scope); + } + + if ($method_id && isset($arg->value->returnType)) { + if (!self::_isCorrectType($arg->value->returnType, $method_id, $i)) { + throw new CodeException('Argument ' . ($i + 1) . ' of ' . $method_id . ' has incorrect type of ' . $arg->value->returnType, $this->_file_name, $arg->value->getLine()); + } + } + } + } + + protected function _checkConstFetch(PhpParser\Node\Expr\ConstFetch $stmt) + { + + } + + protected function _checkClassConstFetch(PhpParser\Node\Expr\ClassConstFetch $stmt, $types_in_scope) + { + if ($this->_check_consts && $stmt->class instanceof PhpParser\Node\Name && $stmt->class->parts !== ['static']) { + if ($stmt->class->parts === ['self']) { + $absolute_class = $this->_absolute_class; + } + else { + $absolute_class = ClassChecker::getAbsoluteClassFromName($stmt->class, $this->_namespace, $this->_aliased_classes); + } + + $const_id = $absolute_class . '::' . $stmt->name; + + if (!defined($const_id)) { + throw new CodeException('Const ' . $const_id . ' is not defined', $this->_file_name, $stmt->getLine()); + } + } + else if ($stmt->class instanceof PhpParser\Node\Expr) { + $this->_checkExpression($stmt->class, $types_in_scope); + } + } + + protected function _checkStaticPropertyFetch(PhpParser\Node\Expr\StaticPropertyFetch $stmt) + { + + } + + protected function _checkReturn(PhpParser\Node\Stmt\Return_ $stmt, array &$types_in_scope) + { + if ($stmt->expr) { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + } + + protected function _checkTernary(PhpParser\Node\Expr\Ternary $stmt, array &$types_in_scope) + { + $this->_checkCondition($stmt->cond, $types_in_scope); + + $if_types = []; + + if ($stmt->cond instanceof PhpParser\Node\Expr\Instanceof_) { + $if_types = $this->_getInstanceOfTypes($stmt->cond); + } + + if ($stmt->if) { + $this->_checkExpression($stmt->if, array_merge($types_in_scope, $if_types)); + } + + $this->_checkExpression($stmt->else, $types_in_scope); + } + + protected function _checkBooleanNot(PhpParser\Node\Expr\BooleanNot $stmt, array &$types_in_scope) + { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + + protected function _checkEmpty(PhpParser\Node\Expr\Empty_ $stmt, array &$types_in_scope) + { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + + protected function _checkThrow(PhpParser\Node\Stmt\Throw_ $stmt, array &$types_in_scope) + { + $this->_checkExpression($stmt->expr, $types_in_scope); + } + + protected function _checkSwitch(PhpParser\Node\Stmt\Switch_ $stmt, array &$types_in_scope) + { + $this->_checkCondition($stmt->cond, $types_in_scope); + + foreach ($stmt->cases as $case) { + if ($case->cond) { + $this->_checkCondition($case->cond, $types_in_scope); + } + + $this->_checkStatements($case->stmts, $types_in_scope); + } + } + + protected function _checkFunctionCall(PhpParser\Node\Expr\FuncCall $stmt, array &$types_in_scope) + { + $method = $stmt->name; + + if ($method instanceof PhpParser\Node\Name) { + if ($method->parts === ['method_exists']) { + $this->_check_methods = false; + } + else if ($method->parts === ['defined']) { + $this->_check_consts = false; + } + } + + foreach ($stmt->args as $i => $arg) { + if ($arg->value instanceof PhpParser\Node\Expr\Variable) { + $this->_checkVariable($arg->value, $types_in_scope, $method, $i); + } + else { + $this->_checkExpression($arg->value, $types_in_scope); + } + } + } + + protected function _checkArrayAccess(PhpParser\Node\Expr\ArrayDimFetch $stmt, array &$types_in_scope) + { + $this->_checkExpression($stmt->var, $types_in_scope); + if ($stmt->dim) { + $this->_checkExpression($stmt->dim, $types_in_scope); + } + } + + protected function _checkEncapsulatedString(PhpParser\Node\Scalar\Encapsed $stmt, array &$types_in_scope) + { + foreach ($stmt->parts as $part) + { + $this->_checkExpression($part, $types_in_scope); + } + } + + protected function _registerMethod(PhpParser\Node\Stmt\ClassMethod $method) + { + $method_id = $this->_absolute_class . '::' . $method->name; + + if (!isset(self::$_method_return_types[$method_id])) { + $comments = self::_parseDocComment($method->getDocComment()); + + $return_types = []; + + if (isset($comments['specials']['return'])) { + $return_blocks = explode(' ', $comments['specials']['return'][0]); + foreach ($return_blocks as $block) { + if ($block) { + if ($block && preg_match('/^\\\?[A-Za-z0-9|\\\]+[A-Za-z0-9]$/', $block)) { + $return_types = explode('|', $block); + break; + } + } + } + } + + $return_types = array_filter($return_types, function($entry) { + return !empty($entry) && $entry !== '[type]'; + }); + + foreach ($return_types as &$return_type) { + if ($return_type[0] === strtoupper($return_type[0])) { + if ($return_type === '$this') { + $return_type = $this->_absolute_class; + } + else { + $return_type = ClassChecker::getAbsoluteClass($return_type, $this->_namespace, $this->_aliased_classes); + } + } + } + + self::$_method_return_types[$method_id] = $return_types; + } + + if (!isset(self::$_method_params[$method_id])) { + self::$_method_params[$method_id] = []; + + foreach ($method->params as $param) { + self::$_method_params[$method_id][] = $param->byRef; + } + } + } + + protected static function _methodExists($method_id) + { + if (isset(self::$_existing_methods[$method_id])) { + return true; + } + + try { + new \ReflectionMethod($method_id); + self::$_existing_methods[$method_id] = 1; + return true; + } + catch (\ReflectionException $e) { + return false; + } + } + + protected function _getMethodReturnTypes($method_id) + { + if (isset(self::$_method_return_types[$method_id])) { + return self::$_method_return_types[$method_id]; + } + + if (!isset(self::$_method_comments[$method_id])) { + self::_extractReflectionMethodInfo($method_id); + } + + $comments = self::_parseDocComment(self::$_method_comments[$method_id]); + + $absolute_class = explode('::', $method_id)[0]; + + $return_types = []; + + if (isset($comments['specials']['return'])) { + $return_blocks = explode(' ', $comments['specials']['return'][0]); + foreach ($return_blocks as $block) { + if ($block && preg_match('/^\\\?[A-Za-z0-9|\\\]+[A-Za-z0-9]$/', $block)) { + $return_types = explode('|', $block); + break; + } + } + } + + $return_types = array_filter($return_types, function($entry) { + return !empty($entry) && $entry !== '[type]'; + }); + + if ($return_types) { + foreach ($return_types as &$return_type) { + if ($return_type[0] === strtoupper($return_type[0])) { + if ($return_type === '$this') { + $return_type = $absolute_class; + } + else if (self::$_declaring_classes[$method_id] === $this->_absolute_class) { + $return_type = ClassChecker::getAbsoluteClass($return_type, $this->_namespace, $this->_aliased_classes); + } + else { + $return_type = FileChecker::getAbsoluteClassInFile($return_type, self::$_method_files[$method_id]); + } + } + } + } + + self::$_method_return_types[$method_id] = $return_types; + + return $return_types; + } + + /** + * Parse a docblock comment into its parts. + * + * Taken from advanced api docmaker + * Which was taken from https://github.com/facebook/libphutil/blob/master/src/parser/docblock/PhutilDocblockParser.php + * + * @return array Array of the main comment and specials + */ + public static function _parseDocComment($docblock) + { + // Strip off comments. + $docblock = trim($docblock); + $docblock = preg_replace('@^/\*\*@', '', $docblock); + $docblock = preg_replace('@\*/$@', '', $docblock); + $docblock = preg_replace('@^\s*\*@m', '', $docblock); + + // Normalize multi-line @specials. + $lines = explode("\n", $docblock); + $last = false; + foreach ($lines as $k => $line) { + if (preg_match('/^\s?@\w/i', $line)) { + $last = $k; + } + else if (preg_match('/^\s*$/', $line)) { + $last = false; + } + else if ($last !== false) { + $lines[$last] = rtrim($lines[$last]).' '.trim($line); + unset($lines[$k]); + } + } + $docblock = implode("\n", $lines); + + $special = array(); + + // Parse @specials. + $matches = null; + $have_specials = preg_match_all('/^\s?@(\w+)\s*([^\n]*)/m', $docblock, $matches, PREG_SET_ORDER); + if ($have_specials) { + $docblock = preg_replace('/^\s?@(\w+)\s*([^\n]*)/m', '', $docblock); + foreach ($matches as $match) { + list($_, $type, $data) = $match; + + if (empty($special[$type])) { + $special[$type] = array(); + } + + $special[$type][] = $data; + } + } + + $docblock = str_replace("\t", ' ', $docblock); + + // Smush the whole docblock to the left edge. + $min_indent = 80; + $indent = 0; + foreach (array_filter(explode("\n", $docblock)) as $line) { + for ($ii = 0; $ii < strlen($line); $ii++) { + if ($line[$ii] != ' ') { + break; + } + $indent++; + } + + $min_indent = min($indent, $min_indent); + } + + $docblock = preg_replace('/^' . str_repeat(' ', $min_indent) . '/m', '', $docblock); + $docblock = rtrim($docblock); + + // Trim any empty lines off the front, but leave the indent level if there + // is one. + $docblock = preg_replace('/^\s*\n/', '', $docblock); + + return array('description' => $docblock, 'specials' => $special); + } + + protected static function _isPassedByRef($method_id, $arg_offset) + { + if (!isset(self::$_method_params[$method_id])) { + self::_extractReflectionMethodInfo($method_id); + } + + return $arg_offset < count(self::$_method_params[$method_id]) && self::$_method_params[$method_id][$arg_offset]; + } + + protected static function _isCorrectType($return_type, $method_id, $arg_offset) + { + if ($return_type === 'mixed') { + return true; + } + + if (!isset(self::$_method_param_types[$method_id])) { + self::_extractReflectionMethodInfo($method_id); + } + + if ($arg_offset >= count(self::$_method_param_types[$method_id])) { + return true; + } + + $expected_type = self::$_method_param_types[$method_id][$arg_offset]; + + if (!$expected_type) { + return true; + } + + if ($return_type === $expected_type) { + return true; + } + + return is_a($return_type, $expected_type, true) || is_a($expected_type, $return_type, true); + } + + protected static function _extractReflectionMethodInfo($method_id) + { + $method = new \ReflectionMethod($method_id); + $params = $method->getParameters(); + + self::$_method_params[$method_id] = []; + self::$_method_param_types[$method_id] = []; + foreach ($params as $param) { + self::$_method_params[$method_id][] = $param->isPassedByReference(); + self::$_method_param_types[$method_id][] = $param->getClass() ? '\\' . $param->getClass()->getName() : null; + } + + self::$_method_comments[$method_id] = $method->getDocComment() ?: ''; + self::$_method_files[$method_id] = $method->getFileName(); + self::$_declaring_classes[$method_id] = '\\' . $method->getDeclaringClass()->name; + } + + /** + * @param string $function + * @return \ReflectionFunction + */ + protected static function _getReflectionFunction($function) + { + if (!isset(self::$_reflection_functions[$function])) { + self::$_reflection_functions[$function] = new \ReflectionFunction($function); + } + + return self::$_reflection_functions[$function]; + } +}