/** * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return string * The class name represented by the given call */ public function visitVar(Node $node) : string { // $$var->method() if ($node->children['name'] instanceof Node) { return ''; } // $this->method() if ($node->children['name'] == 'this') { if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } return (string) $this->context->getClassFQSEN(); } $variable_name = $node->children['name']; if (!$this->context->getScope()->hasVariableWithName($variable_name)) { // Got lost, couldn't find the variable in the current scope // If it really isn't defined, it will be caught by the // undefined var error return ''; } $variable = $this->context->getScope()->getVariableWithName($variable_name); // Hack - loop through the possible types of the var and assume // first found class is correct foreach ($variable->getUnionType()->nonGenericArrayTypes()->getTypeList() as $type) { $child_class_fqsen = FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); if ($this->code_base->hasClassWithFQSEN($child_class_fqsen)) { return (string) FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); } } // We land here if we have a variable // with a native type or no known type. return ''; }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitCall(Node $node) : Context { $expression = $node->children['expr']; (new ContextNode($this->code_base, $this->context, $node))->analyzeBackwardCompatibility(); foreach ($node->children['args']->children ?? [] as $arg_node) { if ($arg_node instanceof Node) { (new ContextNode($this->code_base, $this->context, $arg_node))->analyzeBackwardCompatibility(); } } if ($expression->kind == \ast\AST_VAR) { $variable_name = (new ContextNode($this->code_base, $this->context, $expression))->getVariableName(); if (empty($variable_name)) { return $this->context; } // $var() - hopefully a closure, otherwise we don't know if ($this->context->getScope()->hasVariableWithName($variable_name)) { $variable = $this->context->getScope()->getVariableWithName($variable_name); $union_type = $variable->getUnionType(); if ($union_type->isEmpty()) { return $this->context; } foreach ($union_type->getTypeSet() as $type) { if (!$type instanceof CallableType) { continue; } $closure_fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString((string) $type->asFQSEN()); if ($this->code_base->hasMethod($closure_fqsen)) { // Get the closure $method = $this->code_base->getMethod($closure_fqsen); // Check the call for paraemter and argument types $this->analyzeCallToMethod($this->code_base, $method, $node); } } } } else { if ($expression->kind == \ast\AST_NAME) { try { $method = (new ContextNode($this->code_base, $this->context, $expression))->getFunction($expression->children['name'] ?? $expression->children['method']); } catch (IssueException $exception) { $exception->getIssueInstance()(); return $this->context; } // Check the call for paraemter and argument types $this->analyzeCallToMethod($this->code_base, $method, $node); } else { if ($expression->kind == \ast\AST_CALL || $expression->kind == \ast\AST_STATIC_CALL || $expression->kind == \ast\AST_NEW || $expression->kind == \ast\AST_METHOD_CALL) { $class_list = (new ContextNode($this->code_base, $this->context, $expression))->getClassList(); foreach ($class_list as $class) { if (!$class->hasMethodWithName($this->code_base, '__invoke')) { continue; } $method = $class->getMethodByNameInContext($this->code_base, '__invoke', $this->context); // Check the call for paraemter and argument types $this->analyzeCallToMethod($this->code_base, $method, $node); } } } } return $this->context; }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitVar(Node $node) : Context { $variable_name = AST::variableName($node); // Check to see if the variable already exists if ($this->context->getScope()->hasVariableWithName($variable_name)) { $variable = $this->context->getScope()->getVariableWithName($variable_name); // If we're assigning to an array element then we don't // know what the constitutation of the parameter is // outside of the scope of this assignment, so we add to // its union type rather than replace it. if ($this->is_dim_assignment) { $variable->getUnionType()->addUnionType($this->right_type); } else { // If the variable isn't a pass-by-reference paramter // we clone it so as to not disturb its previous types // as we replace it. if (!($variable instanceof Parameter && $variable->isPassByReference())) { $variable = clone $variable; } $variable->setUnionType($this->right_type); } $this->context->addScopeVariable($variable); return $this->context; } $variable = Variable::fromNodeInContext($this->assignment_node, $this->context, $this->code_base); // Set that type on the variable $variable->getUnionType()->addUnionType($this->right_type); // Note that we're not creating a new scope, just // adding variables to the existing scope $this->context->addScopeVariable($variable); return $this->context; }
/** * @return Variable * A variable in scope or a new variable * * @throws NodeException * An exception is thrown if we can't understand the node * * @throws IssueException * A IssueException is thrown if the variable doesn't * exist */ public function getVariable() : Variable { // Get the name of the variable $variable_name = $this->getVariableName(); if (empty($variable_name)) { throw new NodeException($this->node, "Variable name not found"); } // Check to see if the variable exists in this scope if (!$this->context->getScope()->hasVariableWithName($variable_name)) { throw new IssueException(Issue::fromType(Issue::UndeclaredVariable)($this->context->getFile(), $this->node->lineno ?? 0, [$variable_name])); } return $this->context->getScope()->getVariableWithName($variable_name); }
/** * @return Variable * A variable in scope or a new variable * * @throws NodeException * An exception is thrown if we can't understand the node * * @throws CodeBaseException * A CodeBaseException is thrown if the variable doesn't * exist */ public function getVariable() : Variable { // Get the name of the variable $variable_name = $this->getVariableName(); if (empty($variable_name)) { throw new NodeException($this->node, "Variable name not found"); } // Check to see if the variable exists in this scope if (!$this->context->getScope()->hasVariableWithName($variable_name)) { throw new CodeBaseException(null, "Variable with name {$variable_name} doesn't exist in {$this->context}"); } return $this->context->getScope()->getVariableWithName($variable_name); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitCall(Node $node) : Context { $expression = $node->children['expr']; if (Config::get()->backward_compatibility_checks) { AST::backwardCompatibilityCheck($this->context, $node); foreach ($node->children['args']->children as $arg_node) { if ($arg_node instanceof Node) { AST::backwardCompatibilityCheck($this->context, $arg_node); } } } if ($expression->kind == \ast\AST_NAME) { try { $method = AST::functionFromNameInContext($expression->children['name'], $this->context, $this->code_base); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, $exception->getMessage(), $this->context->getFile(), $node->lineno); return $this->context; } // Check the call for paraemter and argument types $this->analyzeCallToMethod($this->code_base, $method, $node); } else { if ($expression->kind == \ast\AST_VAR) { $variable_name = AST::variableName($expression); if (empty($variable_name)) { return $this->context; } // $var() - hopefully a closure, otherwise we don't know if ($this->context->getScope()->hasVariableWithName($variable_name)) { $variable = $this->context->getScope()->getVariableWithName($variable_name); $union_type = $variable->getUnionType(); if ($union_type->isEmpty()) { return $this->context; } $type = $union_type->head(); if (!$type instanceof CallableType) { return $this->context; } $closure_fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString((string) $type->asFQSEN()); if ($this->code_base->hasMethod($closure_fqsen)) { // Get the closure $method = $this->code_base->getMethod($closure_fqsen); // Check the call for paraemter and argument types $this->analyzeCallToMethod($this->code_base, $method, $node); } } } } return $this->context; }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitVar(Node $node) : Context { $variable_name = AST::variableName($node); // Check to see if the variable already exists if ($this->context->getScope()->hasVariableWithName($variable_name)) { $variable = $this->context->getScope()->getVariableWithName($variable_name); $variable->setUnionType($this->right_type); $this->context->addScopeVariable($variable); return $this->context; } $variable = Variable::fromNodeInContext($this->assignment_node, $this->context, $this->code_base); // Set that type on the variable $variable->getUnionType()->addUnionType($this->right_type); // Note that we're not creating a new scope, just // adding variables to the existing scope $this->context->addScopeVariable($variable); return $this->context; }
/** * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return string * The class name represented by the given call */ public function visitVar(Node $node) : string { // $$var->method() if ($node->children['name'] instanceof Node) { return ''; } // $this->method() if ($node->children['name'] == 'this') { if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } return (string) $this->context->getClassFQSEN(); } $variable_name = $node->children['name']; if (!$this->context->getScope()->hasVariableWithName($variable_name)) { // Got lost, couldn't find the variable in the current scope // If it really isn't defined, it will be caught by the // undefined var error return ''; } $variable = $this->context->getScope()->getVariableWithName($variable_name); $union_type = $variable->getUnionType()->nonNativeTypes()->nonGenericArrayTypes(); // If there are no candidate classes, we'll emit whatever // we have so that we can differentiate between // no-known-type and a shitty type if ($union_type->isEmpty()) { if (!$variable->getUnionType()->isEmpty() && !$variable->getUnionType()->hasType(MixedType::instance()) && !$variable->getUnionType()->hasType(ArrayType::instance()) && !$variable->getUnionType()->hasType(ObjectType::instance())) { $type = (string) $variable->getUnionType(); throw new TypeException("Calling method on non-class type {$type}"); } // No viable class types for the variable. return ''; } $class_fqsen = $this->chooseSingleFQSEN(array_map(function (Type $type) { return $type->asFQSEN(); }, $union_type->getTypeList())); if ($this->code_base->hasClassWithFQSEN($class_fqsen)) { return (string) $class_fqsen; } // We couldn't find any viable classes return ''; }
/** * Visit a node with kind `\ast\AST_VAR` * * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return UnionType * The set of types that are possibly produced by the * given node */ public function visitVar(Node $node) : UnionType { // $$var or ${...} (whose idea was that anyway?) if ($node->children['name'] instanceof Node && ($node->children['name']->kind == \ast\AST_VAR || $node->children['name']->kind == \ast\AST_BINARY_OP)) { return MixedType::instance()->asUnionType(); } // This is nonsense. Give up. if ($node->children['name'] instanceof Node) { return new UnionType(); } $variable_name = $node->children['name']; if (!$this->context->getScope()->hasVariableWithName($variable_name)) { if (!Variable::isSuperglobalVariableWithName($variable_name)) { Log::err(Log::EVAR, "Variable \${$variable_name} is not defined", $this->context->getFile(), $node->lineno ?? 0); } } else { $variable = $this->context->getScope()->getVariableWithName($variable_name); return $variable->getUnionType(); } return new UnionType(); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitIf(Node $node) : Context { // Get the list of scopes for each branch of the // conditional $scope_list = array_map(function (Context $context) { return $context->getScope(); }, $this->child_context_list); $has_else = array_reduce($node->children ?? [], function (bool $carry, $child_node) { return $carry || $child_node instanceof Node && empty($child_node->children['cond']); }, false); // If we're not guaranteed to hit at least one // branch, mark the incoming scope as a possibility if (!$has_else) { $scope_list[] = $this->context->getScope(); } // If there weren't multiple branches, continue on // as if the conditional never happened if (count($scope_list) < 2) { return array_values($this->child_context_list)[0]; } // Get a list of all variables in all scopes $variable_map = []; foreach ($scope_list as $scope) { foreach ($scope->getVariableMap() as $name => $variable) { $variable_map[$name] = $variable; } } // A function that determins if a variable is defined on // every branch $is_defined_on_all_branches = function (string $variable_name) use($scope_list) { return array_reduce($scope_list, function (bool $has_variable, Scope $scope) use($variable_name) { return $has_variable && $scope->hasVariableWithName($variable_name); }, true); }; // Get the intersection of all types for all versions of // the variable from every side of the branch $common_union_type = function (string $variable_name) use($scope_list) { // Get a list of all variables with the given name from // each scope $variable_list = array_filter(array_map(function (Scope $scope) use($variable_name) { if (!$scope->hasVariableWithName($variable_name)) { return null; } return $scope->getVariableWithName($variable_name); }, $scope_list)); // Get the list of types for each version of the variable $type_set_list = array_map(function (Variable $variable) : Set { return $variable->getUnionType()->getTypeSet(); }, $variable_list); if (count($type_set_list) < 2) { return new UnionType($type_set_list[0] ?? []); } return new UnionType(Set::intersectAll($type_set_list)); }; $scope = new Scope(); foreach ($variable_map as $name => $variable) { // Skip variables that are only partially defined if (!$is_defined_on_all_branches($name)) { continue; } // Limit the type of the variable to the subset // of types that are common to all branches $variable = clone $variable; $variable->setUnionType($common_union_type($name)); // Add the variable to the outgoing scope $scope->addVariable($variable); } // print '<'.implode("\t", $scope_list) . "\n"; // print '>'.$scope."\n"; // Set the new scope with only the variables and types // that are common to all branches return $this->context->withScope($scope); }
/** * @param Node $node * A node that has a reference to a variable * * @param Context $context * The context in which we found the reference * * @param CodeBase $code_base * * @return Variable * A variable in scope or a new variable * * @throws NodeException * An exception is thrown if we can't understand the node */ public static function getOrCreateVariableFromNodeInContext(Node $node, Context $context, CodeBase $code_base) : Variable { // Get the name of the variable $variable_name = self::variableName($node); if (empty($variable_name)) { throw new NodeException($node, "Variable name not found"); } // Check to see if the variable exists in this scope if ($context->getScope()->hasVariableWithName($variable_name)) { return $context->getScope()->getVariableWithName($variable_name); } // Create a new variable $variable = Variable::fromNodeInContext($node, $context, $code_base, false); $context->addScopeVariable($variable); return $variable; }
/** * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return string * The class name represented by the given call */ public function visitMethodCall(Node $node) : string { if ($node->children['expr']->kind == \ast\AST_VAR) { if ($node->children['expr']->children['name'] instanceof Node) { return ''; } // $var->method() if ($node->children['expr']->children['name'] == 'this') { if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } return (string) $this->context->getClassFQSEN(); } $variable_name = $node->children['expr']->children['name']; if (!$this->context->getScope()->hasVariableWithName($variable_name)) { // Got lost, couldn't find the variable in the current scope // If it really isn't defined, it will be caught by the // undefined var error return ''; } $variable = $this->context->getScope()->getVariableWithName($variable_name); // Hack - loop through the possible types of the var and assume // first found class is correct foreach ($variable->getUnionType()->nonGenericArrayTypes()->getTypeList() as $type) { $child_class_fqsen = FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); if ($this->code_base->hasClassWithFQSEN($child_class_fqsen)) { return (string) FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); } } // Could not find name return ''; } if ($node->children['expr']->kind == \ast\AST_PROP) { $prop = $node->children['expr']; if (!($prop->children['expr']->kind == \ast\AST_VAR && !$prop->children['expr']->children['name'] instanceof Node)) { return ''; } // $var->prop->method() $var = $prop->children['expr']; if ($var->children['name'] == 'this') { // If we're not in a class scope, 'this' won't work if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } // Get the class in scope $clazz = $this->code_base->getClassByFQSEN($this->context->getClassFQSEN()); if ($prop->children['prop'] instanceof Node) { // $this->$prop->method() - too dynamic, give up return ''; } $property_name = $prop->children['prop']; if ($clazz->hasPropertyWithName($this->code_base, $property_name)) { try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (AccessException $exception) { Log::err(Log::EACCESS, $exception->getMessage(), $this->context->getFile(), $node->lineno); return ''; } // Find the first viable property type foreach ($property->getUnionType()->nonGenericArrayTypes()->getTypeList() as $type) { $class_fqsen = FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); if ($this->code_base->hasClassWithFQSEN($class_fqsen)) { return (string) $class_fqsen; } } } // No such property was found, or none were classes // that could be found return ''; } return ''; } if ($node->children['expr']->kind == \ast\AST_METHOD_CALL) { // Get the type returned by the first method // call. $union_type = UnionType::fromNode($this->context, $this->code_base, $node->children['expr']); // Find the subset of types that are viable // classes $viable_class_types = $union_type->nonNativeTypes()->nonGenericArrayTypes(); // If there are no non-native types, give up if ($viable_class_types->isEmpty()) { return ''; } // Return the first non-native type in the // list and hope its a class return (string) $viable_class_types->head(); } return ''; }
/** * @param string $type_string * A '|' delimited string representing a type in the form * 'int|string|null|ClassName'. * * @param Context $context * The context in which the type string was * found * * @return UnionType */ public static function fromStringInContext(string $type_string, Context $context) : UnionType { if (empty($type_string)) { return new UnionType(); } // If our scope has a generic type identifier defined on it // that matches the type string, return that UnionType. if ($context->getScope()->hasTemplateType($type_string)) { return $context->getScope()->getTemplateType($type_string)->asUnionType(); } return new UnionType(array_map(function (string $type_name) use($context, $type_string) { assert($type_name !== '', "Type cannot be empty."); return Type::fromStringInContext($type_name, $context); }, array_filter(array_map(function (string $type_name) { return trim($type_name); }, explode('|', $type_string))))); }