/** * @param Node $node * An AST_VAR node * * @param Context $context * The context in which the variable is found * * @param CodeBase $code_base * * @return Variable * A variable begotten from a node */ public static function fromNodeInContext(Node $node, Context $context, CodeBase $code_base, bool $should_check_type = true) : Variable { $variable_name = AST::variableName($node); // Get the type of the assignment $union_type = $should_check_type ? UnionType::fromNode($context, $code_base, $node) : new UnionType(); $variable = new Variable($context->withLineNumberStart($node->lineno ?? 0)->withLineNumberEnd($node->endLineno ?? 0), $variable_name, $union_type, $node->flags); 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 visitNew(Node $node) : string { // Things of the form `new $class_name();` if ($node->children['class']->kind == \ast\AST_VAR) { return ''; } // Anonymous class // $v = new class { ... } if ($node->children['class']->kind == \ast\AST_CLASS && $node->children['class']->flags & \ast\flags\CLASS_ANONYMOUS) { return AST::unqualifiedNameForAnonymousClassNode($node->children['class'], $this->context); } // Things of the form `new $method->name()` if ($node->children['class']->kind !== \ast\AST_NAME) { return ''; } $class_name = $node->children['class']->children['name']; if (!in_array($class_name, ['self', 'static', 'parent'])) { return (string) UnionTypeVisitor::unionTypeFromClassNode($this->code_base, $this->context, $node->children['class']); } if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, "Cannot access {$class_name}:: when no class scope is active", $this->context->getFile(), $node->lineno); return ''; } if ($class_name == 'static') { return (string) $this->context->getClassFQSEN(); } if ($class_name == 'self') { if ($this->context->isGlobalScope()) { assert(false, "Unimplemented branch is required for {$this->context}"); } else { return (string) $this->context->getClassFQSEN(); } } if ($class_name == 'parent') { $clazz = $this->context->getClassInScope($this->code_base); if (!$clazz->hasParentClassFQSEN()) { return ''; } return (string) $clazz->getParentClassFQSEN(); } return ''; }
/** * Analyze the parameters and arguments for a call * to the given method or function * * @param CodeBase $code_base * @param Method $method * @param Node $node * * @return null */ private function analyzeCallToMethod(CodeBase $code_base, Method $method, Node $node) { if (Database::isEnabled()) { // Store the call to the method so we can track // dependencies later (new CalledBy((string) $method->getFQSEN(), $this->context))->write(Database::get()); } // Create variables for any pass-by-reference // parameters $argument_list = $node->children['args']; foreach ($argument_list->children as $i => $argument) { $parameter = $method->getParameterList()[$i] ?? null; if (!$parameter) { continue; } // If pass-by-reference, make sure the variable exists // or create it if it doesn't. if ($parameter->isPassByReference()) { if ($argument->kind == \ast\AST_VAR) { // We don't do anything with it; just create it // if it doesn't exist $variable = AST::getOrCreateVariableFromNodeInContext($argument, $this->context, $this->code_base); } else { if ($argument->kind == \ast\AST_STATIC_PROP || $argument->kind == \ast\AST_PROP) { $property_name = $argument->children['prop']; if (is_string($property_name)) { // We don't do anything with it; just create it // if it doesn't exist try { $property = AST::getOrCreatePropertyFromNodeInContext($argument->children['prop'], $argument, $this->context, $this->code_base); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, $exception->getMessage(), $this->context->getFile(), $node->lineno); } catch (NodeException $exception) { // If we can't figure out what kind of a call // this is, don't worry about it } } else { // This is stuff like `Class->$foo`. I'm ignoring // it. } } } } } // Confirm the argument types are clean ArgumentType::analyze($method, $node, $this->context, $this->code_base); // Take another pass over pass-by-reference parameters // and assign types to passed in variables foreach ($argument_list->children as $i => $argument) { $parameter = $method->getParameterList()[$i] ?? null; if (!$parameter) { continue; } // If the parameter is pass-by-reference and we're // passing a variable in, see if we should pass // the parameter and variable types to eachother $variable = null; if ($parameter->isPassByReference()) { if ($argument->kind == \ast\AST_VAR) { $variable = AST::getOrCreateVariableFromNodeInContext($argument, $this->context, $this->code_base); } else { if ($argument->kind == \ast\AST_STATIC_PROP || $argument->kind == \ast\AST_PROP) { $property_name = $argument->children['prop']; if (is_string($property_name)) { // We don't do anything with it; just create it // if it doesn't exist try { $variable = AST::getOrCreatePropertyFromNodeInContext($argument->children['prop'], $argument, $this->context, $this->code_base); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, $exception->getMessage(), $this->context->getFile(), $node->lineno); } catch (NodeException $exception) { // If we can't figure out what kind of a call // this is, don't worry about it } } else { // This is stuff like `Class->$foo`. I'm ignoring // it. } } } if ($variable) { $variable->getUnionType()->addUnionType($parameter->getUnionType()); } } } // If we're in quick mode, don't retest methods based on // parameter types passed in if (Config::get()->quick_mode) { return; } // We're going to hunt to see if any of the arguments // have a mismatch with the parameters. If so, we'll // re-check the method to see how the parameters impact // its return type $has_argument_parameter_mismatch = false; // Now that we've made sure the arguments are sufficient // for definitions on the method, we iterate over the // arguments again and add their types to the parameter // types so we can test the method again $argument_list = $node->children['args']; // We create a copy of the parameter list so we can switch // back to it after $original_parameter_list = $method->getParameterList(); foreach ($argument_list->children as $i => $argument) { $parameter = $method->getParameterList()[$i] ?? null; if (!$parameter) { continue; } // If the parameter has no type, pass the // argument's type to it if ($parameter->getUnionType()->isEmpty()) { $has_argument_parameter_mismatch = true; $argument_type = UnionType::fromNode($this->context, $this->code_base, $argument); // If this isn't an internal function or method // and it has no type, add the argument's type // to it so we can compare it to subsequent // calls if (!$parameter->getContext()->isInternal()) { // Clone the parameter in the original // parameter list so we can reset it // later $original_parameter_list[$i] = clone $parameter; // Then set the new type on that parameter based // on the argument's type. We'll use this to // retest the method with the passed in types $parameter->getUnionType()->addUnionType($argument_type); } } } // Now that we know something about the parameters used // to call the method, we can reanalyze the method with // the types of the parameter, making sure we don't get // into an infinite loop of checking calls to the current // method in scope if ($has_argument_parameter_mismatch && !$method->getContext()->isInternal() && (!$this->context->isMethodScope() || $method->getFQSEN() !== $this->context->getMethodFQSEN())) { $method->analyze($method->getContext(), $code_base); } // Reset to the original parameter list after having // tested the parameters with the types passed in $method->setParameterList($original_parameter_list); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitCatch(Node $node) : Context { // Get the name of the class $class_name = $node->children['class']->children['name']; $clazz = null; // If we can't figure out the class name (which happens // from time to time), then give up if (!empty($class_name)) { $class_fqsen = FullyQualifiedClassName::fromStringInContext($class_name, $this->context); // Check to see if the class actually exists if ($this->code_base->hasClassWithFQSEN($class_fqsen)) { $clazz = $this->code_base->getClassByFQSEN($class_fqsen); } else { Log::err(Log::EUNDEF, "call to method on undeclared class {$class_name}", $this->context->getFile(), $node->lineno); } } $variable_name = AST::variableName($node->children['var']); if (!empty($variable_name)) { $variable = Variable::fromNodeInContext($node->children['var'], $this->context, $this->code_base, false); if ($clazz) { $variable->setUnionType($clazz->getUnionType()); } $this->context->addScopeVariable($variable); } return $this->context; }
/** * @param CodeBase $code_base * The global code base * * @param Method $method * The method we're analyzing arguments for * * @param Node $node * The node holding the method call we're looking at * * @param Context $context * The context in which we see the call * * @return null * * @see \Phan\Deprecated\Pass2::arglist_type_check * Formerly `function arglist_type_check` */ private static function analyzeParameterList(CodeBase $code_base, Method $method, Node $node, Context $context) { foreach ($node->children ?? [] as $i => $argument) { // Get the parameter associated with this argument $parameter = $method->getParameterList()[$i] ?? null; // This issue should be caught elsewhere if (!$parameter) { continue; } // If this is a pass-by-reference parameter, make sure // we're passing an allowable argument if ($parameter->isPassByReference()) { if (!$argument instanceof \ast\Node || $argument->kind != \ast\AST_VAR && $argument->kind != \ast\AST_DIM && $argument->kind != \ast\AST_PROP && $argument->kind != \ast\AST_STATIC_PROP) { Log::err(Log::ETYPE, "Only variables can be passed by reference at arg#" . ($i + 1) . " of {$method->getFQSEN()}()", $context->getFile(), $node->lineno); } else { $variable_name = AST::variableName($argument); if ($argument->kind == \ast\AST_STATIC_PROP) { if (in_array($variable_name, ['self', 'static', 'parent'])) { Log::err(Log::ESTATIC, "Using {$variable_name}:: when not in object context", $context->getFile(), $argument->lineno); } } } } // Get the type of the argument. We'll check it against // the parameter in a moment $argument_type = UnionType::fromNode($context, $code_base, $argument); // Expand it to include all parent types up the chain $argument_type_expanded = $argument_type->asExpandedTypes($code_base); /* TODO see issue #42 If argument is an object and it has a String union type, then we need to ignore that in strict_types=1 mode. if ($argument instanceof \ast\Node) { if(!empty($argument->children['class'])) { // arg is an object if ($method->getContext()->getStrictTypes()) { ... } } } or maybe UnionType::fromNode should check strict_types and not return the string union type or we shouldn't add the string type at all when a class has a __toString() and instead set a flag and check that instead */ // Check the method to see if it has the correct // parameter types. If not, keep hunting through // alternates of the method until we find one that // takes the correct types $alternate_parameter = null; $alternate_found = false; foreach ($method->alternateGenerator($code_base) as $alternate_id => $alternate_method) { if (empty($alternate_method->getParameterList()[$i])) { continue; } // Get the parameter associated with this argument $alternate_parameter = $alternate_method->getParameterList()[$i] ?? null; // Expand the types to find all parents and traits $alternate_parameter_type_expanded = $alternate_parameter->getUnionType()->asExpandedTypes($code_base); // See if the argument can be cast to the // parameter if ($argument_type_expanded->canCastToUnionType($alternate_parameter_type_expanded)) { $alternate_found = true; break; } } if (!$alternate_found) { $parameter_name = $alternate_parameter ? $alternate_parameter->getName() : 'unknown'; $parameter_type = $alternate_parameter ? $alternate_parameter->getUnionType() : 'unknown'; if ($method->getContext()->isInternal()) { Log::err(Log::ETYPE, "arg#" . ($i + 1) . "({$parameter_name}) is " . "{$argument_type_expanded} but {$method->getFQSEN()}() " . "takes {$parameter_type}", $context->getFile(), $node->lineno); } else { Log::err(Log::ETYPE, "arg#" . ($i + 1) . "({$parameter_name}) is " . "{$argument_type_expanded} but {$method->getFQSEN()}() " . "takes {$parameter_type} " . "defined at {$method->getContext()->getFile()}:{$method->getContext()->getLineNumberStart()}", $context->getFile(), $node->lineno); } } } }
/** * ast_node_type() is for places where an actual type * name appears. This returns that type name. Use node_type() * instead to figure out the type of a node * * @param Context $context * @param null|string|Node $node * * @see \Phan\Deprecated\AST::ast_node_type */ public static function fromSimpleNode(Context $context, $node) : UnionType { return AST::unionTypeFromSimpleNode($context, $node); }
/** * @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 visitProp(Node $node) : string { if (!($node->children['expr']->kind == \ast\AST_VAR && !$node->children['expr']->children['name'] instanceof Node)) { return ''; } // $var->prop->method() $var = $node->children['expr']; $class = null; 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 ''; } // $this->$node->method() if ($node->children['prop'] instanceof Node) { // Too hard. Giving up. return ''; } $class = $this->context->getClassInScope($this->code_base); } else { // Get the list of viable class types for the // variable $union_type = AST::varUnionType($this->context, $var)->nonNativeTypes()->nonGenericArrayTypes(); if ($union_type->isEmpty()) { return ''; } $class_fqsen = $union_type->head()->asFQSEN(); if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) { return ''; } $class = $this->code_base->getClassByFQSEN($class_fqsen); } $property_name = $node->children['prop']; if (!$class->hasPropertyWithName($this->code_base, $property_name)) { // If we can't find the property, there's // no type. Thie issue should be caught // elsewhere. return ''; } try { $property = $class->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (AccessException $exception) { Log::err(Log::EACCESS, $exception->getMessage(), $this->context->getFile(), $node->lineno); return ''; } $union_type = $property->getUnionType()->nonNativeTypes(); if ($union_type->isEmpty()) { // If we don't have a type on the property we // can't figure out the class type. return ''; } else { // Return the first type on the property // that could be a reference to a class return (string) $union_type->head()->asFQSEN(); } // No such property was found, or none were classes // that could be found return ''; }
private function visitClassNode(Node $node) : UnionType { // Things of the form `new $class_name();` if ($node->kind == \ast\AST_VAR) { return new UnionType(); } // Anonymous class of form `new class { ... }` if ($node->kind == \ast\AST_CLASS && $node->flags & \ast\flags\CLASS_ANONYMOUS) { // Generate a stable name for the anonymous class $anonymous_class_name = AST::unqualifiedNameForAnonymousClassNode($node, $this->context); // Turn that into a fully qualified name $fqsen = FullyQualifiedClassName::fromStringInContext($anonymous_class_name, $this->context); // Turn that into a union type return Type::fromFullyQualifiedString($fqsen)->asUnionType(); } // Things of the form `new $method->name()` if ($node->kind !== \ast\AST_NAME) { return new UnionType(); } // Get the name of the class $class_name = $node->children['name']; // If this is a straight-forward class name, recurse into the // class node and get its type if (!Type::isSelfTypeString($class_name)) { // TODO: does anyone else call this method? return self::unionTypeFromClassNode($this->code_base, $this->context, $node); } // This is a self-referential node if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, "Cannot access {$class_name} when not in a class scope", $this->context->getFile(), $node->lineno); return new UnionType(); } // Reference to a parent class if ($class_name === 'parent') { $class = $this->context->getClassInScope($this->code_base); if (!$class->hasParentClassFQSEN()) { Log::err(Log::ESTATIC, "Reference to parent of parentless class {$class->getFQSEN()}", $this->context->getFile(), $node->lineno); return new UnionType(); } return Type::fromFullyQualifiedString((string) $class->getParentClassFQSEN())->asUnionType(); } return Type::fromFullyQualifiedString((string) $this->context->getClassFQSEN())->asUnionType(); }
/** * @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; } if ($this->context->getIsConditional()) { // If we're within a conditional, we shouldn't // replace the type since the other side of // the branch may have another type $variable->getUnionType()->addUnionType($this->right_type); } else { $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; }
/** * Perform some backwards compatibility checks on a node * * @param Context $context * The context in which the node appears * * @param Node $node * The node we'd like to check * * @return null * * @see \Phan\Deprecated::bc_check * Formerly `function bc_check` */ public static function backwardCompatibilityCheck(Context $context, Node $node) { if (empty($node->children['expr'])) { return; } if ($node->kind !== \ast\AST_DIM) { if (!$node->children['expr'] instanceof Node) { return; } if ($node->children['expr']->kind !== \ast\AST_DIM) { AST::backwardCompatibilityCheck($context, $node->children['expr']); return; } $temp = $node->children['expr']->children['expr']; $lnode = $temp; } else { $temp = $node->children['expr']; $lnode = $temp; } if (!($temp->kind == \ast\AST_PROP || $temp->kind == \ast\AST_STATIC_PROP)) { return; } while ($temp instanceof Node && ($temp->kind == \ast\AST_PROP || $temp->kind == \ast\AST_STATIC_PROP)) { $lnode = $temp; // Lets just hope the 0th is the expression // we want $temp = array_values($temp->children)[0]; } if (!$temp instanceof Node) { return; } // Foo::$bar['baz'](); is a problem // Foo::$bar['baz'] is not if ($lnode->kind === \ast\AST_STATIC_PROP && $node->kind !== \ast\AST_CALL) { return; } if (($lnode->children['prop'] instanceof Node && $lnode->children['prop']->kind == \ast\AST_VAR || !empty($lnode->children['class']) && $lnode->children['class'] instanceof Node && ($lnode->children['class']->kind == \ast\AST_VAR || $lnode->children['class']->kind == \ast\AST_NAME)) && ($temp->kind == \ast\AST_VAR || $temp->kind == \ast\AST_NAME)) { $ftemp = new \SplFileObject($context->getFile()); $ftemp->seek($node->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '}[') === false || strpos($line, ']}') === false || strpos($line, '>{') === false) { Log::err(Log::ECOMPAT, "expression may not be PHP 7 compatible", $context->getFile(), $node->lineno ?? 0); } } }
/** * Visit a node with kind `\ast\AST_METHOD_CALL` * * @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 visitMethodCall(Node $node) : UnionType { $class_name = AST::classNameFromNode($this->context, $this->code_base, $node); if (empty($class_name)) { return new UnionType(); } $class_fqsen = FullyQualifiedClassName::fromstringInContext($class_name, $this->context); assert($this->code_base->hasClassWithFQSEN($class_fqsen), "Class {$class_fqsen} must exist"); $clazz = $this->code_base->getClassByFQSEN($class_fqsen); $method_name = $node->children['method']; // Give up on any complicated nonsense where the // method name is a variable such as in // `$variable->$function_name()`. if ($method_name instanceof Node) { return new UnionType(); } // Method names can some times turn up being // other method calls. assert(is_string($method_name), "Method name must be a string. Something else given."); if (!$clazz->hasMethodWithName($this->code_base, $method_name)) { Log::err(Log::EUNDEF, "call to undeclared method {$class_fqsen}->{$method_name}()", $this->context->getFile(), $node->lineno); return new UnionType(); } $method = $clazz->getMethodByNameInContext($this->code_base, $method_name, $this->context); return $method->getUnionType(); }
/** * Visit a node with kind `\ast\AST_RETURN` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitReturn(Node $node) : Context { if (Config::get()->backward_compatibility_checks) { AST::backwardCompatibilityCheck($this->context, $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 visitCatch(Node $node) : Context { try { $union_type = UnionTypeVisitor::unionTypeFromClassNode($this->code_base, $this->context, $node->children['class']); $class_list = AST::classListFromNodeInContext($this->code_base, $this->context, $node->children['class']); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, "catching undeclared class {$exception->getFQSEN()}", $this->context->getFile(), $node->lineno); } $variable_name = AST::variableName($node->children['var']); if (!empty($variable_name)) { $variable = Variable::fromNodeInContext($node->children['var'], $this->context, $this->code_base, false); if (!$union_type->isEmpty()) { $variable->setUnionType($union_type); } $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 visitInstanceOf(Node $node) : string { if ($node->children[1]->kind == \ast\AST_NAME) { return AST::qualifiedName($this->context, $node); } return ''; }
/** * @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; }