public static fromNode ( |
||
$context | The context of the parser at the node for which we'd like to determine a type | |
$code_base | The code base within which we're operating | |
$node | ast\Node | string | null | The node for which we'd like to determine its type |
$should_catch_issue_exception | boolean | Set to true to cause loggable issues to be thrown instead of emitted as issues to the log. |
return |
/** * @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; }
/** * Force the future to figure out the type of the * given object or throw an IssueException if it * is unable to do so * * @return UnionType * The type of the future * * @throws IssueException * An exception is thrown if we are unable to determine * the type at the time this method is called */ public function get() : UnionType { return UnionType::fromNode($this->context, $this->code_base, $this->node, false); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitForeach(Node $node) : Context { if ($node->children['value']->kind == \ast\AST_LIST) { foreach ($node->children['value']->children as $child_node) { $variable = Variable::fromNodeInContext($child_node, $this->context, $this->code_base, false); $this->context->addScopeVariable($variable); } // Otherwise, read the value as regular variable and // add it to the scope } else { // Create a variable for the value $variable = Variable::fromNodeInContext($node->children['value'], $this->context, $this->code_base, false); // Get the type of the node from the left side $type = UnionType::fromNode($this->context, $this->code_base, $node->children['expr']); // Filter out the non-generic types of the // expression $non_generic_type = $type->asNonGenericTypes(); // If we were able to figure out the type and its // a generic type, then set its element types as // the type of the variable if (!$non_generic_type->isEmpty()) { $variable->setUnionType($non_generic_type); } // Add the variable to the scope $this->context->addScopeVariable($variable); } // If there's a key, make a variable out of that too if (!empty($node->children['key'])) { if ($node->children['key'] instanceof \ast\Node && $node->children['key']->kind == \ast\AST_LIST) { Log::err(Log::EFATAL, "Can't use list() as a key element - aborting", $this->context->getFile(), $node->lineno); } $variable = Variable::fromNodeInContext($node->children['key'], $this->context, $this->code_base, false); $this->context->addScopeVariable($variable); } // Note that we're not creating a new scope, just // adding variables to the existing scope return $this->context; }
/** * Check to see if the given Clazz is a duplicate * * @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 * * @param CodeBase $code_base * * @return null * * @see \Phan\Deprecated\Pass2::arg_check * Formerly `function arg_check` */ private static function analyzeInternalArgumentType(Method $method, Node $node, Context $context, CodeBase $code_base) { $arglist = $node->children['args']; $argcount = count($arglist->children); switch ($method->getName()) { case 'join': case 'implode': // (string glue, array pieces), // (array pieces, string glue) or // (array pieces) if ($argcount == 1) { self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(pieces) is %s but {$method->getFQSEN()}() takes array when passed only 1 arg"); return; } else { if ($argcount == 2) { $arg1_type = UnionType::fromNode($context, $code_base, $arglist->children[0]); $arg2_type = UnionType::fromNode($context, $code_base, $arglist->children[1]); if ((string) $arg1_type == 'array') { if (!$arg1_type->canCastToUnionType(StringType::instance()->asUnionType())) { Log::err(Log::EPARAM, "arg#2(glue) is {$arg2_type} but {$method->getFQSEN()}() takes string when arg#1 is array", $context->getFile(), $context->getLineNumberStart()); } } else { if ((string) $arg1_type == 'string') { if (!$arg2_type->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::EPARAM, "arg#2(pieces) is {$arg2_type} but {$method->getFQSEN()}() takes array when arg#1 is string", $context->getFile(), $context->getLineNumberStart()); } } } return; } } // Any other arg counts we will let the regular // checks handle break; case 'array_udiff': case 'array_diff_uassoc': case 'array_uintersect_assoc': case 'array_intersect_ukey': if ($argcount < 3) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$method->getFQSEN()}() which requires {$method->getNumberOfRequiredParameters()} arg(s)", $context->getFile(), $context->getLineNumberStart()); return; } self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), "The last argument to {$method->getFQSEN()} must be a callable"); for ($i = 0; $i < $argcount - 1; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, CallableType::instance()->asUnionType(), "arg#" . ($i + 1) . " is %s but {$method->getFQSEN()}() takes array"); } return; case 'array_diff_uassoc': case 'array_uintersect_uassoc': if ($argcount < 4) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$method->getFQSEN()}() which requires {$method->getNumberOfRequiredParameters()} arg(s)", $context->getFile(), $context->getLineNumberStart()); return; } // The last 2 arguments must be a callable and there // can be a variable number of arrays before it self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), "The last argument to {$method->getFQSEN()} must be a callable"); self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 2], $context, $code_base, CallableType::instance()->asUnionType(), "The second last argument to {$method->getFQSEN()} must be a callable"); for ($i = 0; $i < $argcount - 2; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#" . ($i + 1) . " is %s but {$method->getFQSEN()}() takes array"); } return; case 'strtok': // (string str, string token) or (string token) if ($argcount == 1) { // If we have just one arg it must be a string token self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(token) is %s but {$method->getFQSEN()}() takes string when passed only one arg"); } // The arginfo check will handle the other case break; case 'min': case 'max': if ($argcount == 1) { // If we have just one arg it must be an array if (!self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(values) is %s but {$method->getFQSEN()}() takes array when passed only one arg")) { return; } } // The arginfo check will handle the other case break; default: break; } }
/** * 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 { $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."); try { $class_fqsen = null; foreach ($this->classListFromNode($node->children['class'] ?? $node->children['expr']) as $i => $class) { $class_fqsen = $class->getFQSEN(); if (!$class->hasMethodWithName($this->code_base, $method_name)) { continue; } try { $method = $class->getMethodByNameInContext($this->code_base, $method_name, $this->context); $union_type = $method->getUnionType(); // Map template types to concrete types if ($union_type->hasTemplateType()) { // Get the type of the object calling the property $expression_type = UnionType::fromNode($this->context, $this->code_base, $node->children['expr']); // Map template types to concrete types $union_type = $union_type->withTemplateParameterTypeMap($expression_type->getTemplateParameterTypeMap($this->code_base)); } // Remove any references to \static or \static[] // once we're talking about the method's return // type outside of its class if ($union_type->hasStaticType()) { $union_type = clone $union_type; $union_type->removeType(\Phan\Language\Type\StaticType::instance()); } if ($union_type->genericArrayElementTypes()->hasStaticType()) { $union_type = clone $union_type; // Find the static type on the list $static_type = $union_type->getTypeSet()->find(function (Type $type) : bool { return $type->isGenericArray() && $type->genericArrayElementType()->isStaticType(); }); // Remove it from the list $union_type->removeType($static_type); } return $union_type; } catch (IssueException $exception) { return new UnionType(); } } } catch (IssueException $exception) { // Swallow it } catch (CodeBaseException $exception) { $this->emitIssue(Issue::UndeclaredClassMethod, $node->lineno ?? 0, $method_name, (string) $exception->getFQSEN()); } return new UnionType(); }
/** * 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); }
/** * Visit a node with kind `\ast\AST_PROP_DECL` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitPropDecl(Node $node) : Context { // Bomb out if we're not in a class context $clazz = $this->getContextClass(); // Get a comment on the property declaration $comment = Comment::fromStringInContext($node->children[0]->docComment ?? '', $this->context); foreach ($node->children ?? [] as $i => $child_node) { // Ignore children which are not property elements if (!$child_node || $child_node->kind != \ast\AST_PROP_ELEM) { continue; } // If something goes wrong will getting the type of // a property, we'll store it as a future union // type and try to figure it out later $future_union_type = null; try { // Get the type of the default $union_type = UnionType::fromNode($this->context, $this->code_base, $child_node->children['default'], false); } catch (IssueException $exception) { $future_union_type = new FutureUnionType($this->code_base, $this->context, $child_node->children['default']); $union_type = new UnionType(); } // Don't set 'null' as the type if thats the default // given that its the default default. if ($union_type->isType(NullType::instance())) { $union_type = new UnionType(); } $property_name = $child_node->children['name']; assert(is_string($property_name), 'Property name must be a string. ' . 'Got ' . print_r($property_name, true) . ' at ' . $this->context); $property = new Property(clone $this->context->withLineNumberStart($child_node->lineno ?? 0), is_string($child_node->children['name']) ? $child_node->children['name'] : '_error_', $union_type, $node->flags ?? 0); $property->setFQSEN(FullyQualifiedPropertyName::make($clazz->getFQSEN(), $property->getName())); // Add the property to the class $clazz->addProperty($this->code_base, $property); $property->setSuppressIssueList($comment->getSuppressIssueList()); // Look for any @var declarations if ($variable = $comment->getVariableList()[$i] ?? null) { if ((string) $union_type != 'null' && !$union_type->canCastToUnionType($variable->getUnionType())) { $this->emitIssue(Issue::TypeMismatchProperty, $child_node->lineno ?? 0, (string) $union_type, (string) $property->getFQSEN(), (string) $variable->getUnionType()); } // Set the declared type to the doc-comment type and add // |null if the default value is null $property->getUnionType()->addUnionType($variable->getUnionType()); } // Wait until after we've added the (at)var type // before setting the future so that calling // $property->getUnionType() doesn't force the // future to be reified. if (!empty($future_union_type)) { $property->setFutureUnionType($future_union_type); } } 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 visitInstanceof(Node $node) : Context { // Only look at things of the form // `$variable instanceof ClassName` if ($node->children['expr']->kind !== \ast\AST_VAR) { return $this->context; } $context = $this->context; try { // Get the variable we're operating on $variable = (new ContextNode($this->code_base, $this->context, $node->children['expr']))->getVariable(); // Get the type that we're checking it against $type = UnionType::fromNode($this->context, $this->code_base, $node->children['class']); // Make a copy of the variable $variable = clone $variable; // Add the type to the variable $variable->getUnionType()->addUnionType($type); // Overwrite the variable with its new type $context = $context->withScopeVariable($variable); } catch (\Exception $exception) { // Swallow it } return $context; }
/** * Check to see if the given Clazz is a duplicate * * @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 * * @param CodeBase $code_base * * @return null * * @see \Phan\Deprecated\Pass2::arg_check * Formerly `function arg_check` */ private static function analyzeInternalArgumentType(Method $method, Node $node, Context $context, CodeBase $code_base) { $arglist = $node->children['args']; $argcount = count($arglist->children); switch ($method->getName()) { case 'join': case 'implode': // (string glue, array pieces), // (array pieces, string glue) or // (array pieces) if ($argcount == 1) { self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "arg#1(pieces) is %s but {$method->getFQSEN()}() takes array when passed only 1 arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'pieces', (string) $method->getFQSEN(), 'string', 'array']); }); return; } else { if ($argcount == 2) { $arg1_type = UnionType::fromNode($context, $code_base, $arglist->children[0]); $arg2_type = UnionType::fromNode($context, $code_base, $arglist->children[1]); if ((string) $arg1_type == 'array') { if (!$arg1_type->canCastToUnionType(StringType::instance()->asUnionType())) { Issue::emit(Issue::ParamSpecial1, $context->getFile(), $context->getLineNumberStart(), 2, 'glue', (string) $arg2_type, (string) $method->getFQSEN(), 'string', 1, 'array'); } } else { if ((string) $arg1_type == 'string') { if (!$arg2_type->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::emit(Issue::ParamSpecial1, $context->getFile(), $context->getLineNumberStart(), 2, 'pieces', (string) $arg2_type, (string) $method->getFQSEN(), 'array', 1, 'string'); } } } return; } } // Any other arg counts we will let the regular // checks handle break; case 'array_udiff': case 'array_diff_uassoc': case 'array_uintersect_assoc': case 'array_intersect_ukey': if ($argcount < 3) { Issue::emit(Issue::ParamTooFewInternal, $context->getFile(), $context->getLineNumberStart(), $argcount, (string) $method->getFQSEN(), $method->getNumberOfRequiredParameters()); return; } self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "The last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial3)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); for ($i = 0; $i < $argcount - 1; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method, $i) { // "arg#".($i+1)." is %s but {$method->getFQSEN()}() takes array" return Issue::fromType(Issue::ParamTypeMismatch)($context->getFile(), $context->getLineNumberStart(), [$i + 1, (string) $node_type, (string) $method->getFQSEN(), 'array']); }); } return; case 'array_diff_uassoc': case 'array_uintersect_uassoc': if ($argcount < 4) { Issue::emit(Issue::ParamTooFewInternal, $context->getFile(), $context->getLineNumberStart(), $argcount, (string) $method->getFQSEN(), $method->getNumberOfRequiredParameters()); return; } // The last 2 arguments must be a callable and there // can be a variable number of arrays before it self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "The last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial3)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 2], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "The second last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial4)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); for ($i = 0; $i < $argcount - 2; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method, $i) { // "arg#".($i+1)." is %s but {$method->getFQSEN()}() takes array" return Issue::fromType(Issue::ParamTypeMismatch)($context->getFile(), $context->getLineNumberStart(), [$i + 1, (string) $node_type, (string) $method->getFQSEN(), 'array']); }); } return; case 'strtok': // (string str, string token) or (string token) if ($argcount == 1) { // If we have just one arg it must be a string token self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "arg#1(token) is %s but {$method->getFQSEN()}() takes string when passed only one arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'token', (string) $node_type, (string) $method->getFQSEN(), 'string']); }); } // The arginfo check will handle the other case break; case 'min': case 'max': if ($argcount == 1) { // If we have just one arg it must be an array if (!self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "arg#1(values) is %s but {$method->getFQSEN()}() takes array when passed only one arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'values', (string) $node_type, (string) $method->getFQSEN(), 'array']); })) { return; } } // The arginfo check will handle the other case break; default: break; } }
/** * @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 ''; }
/** * Visit a node with kind `\ast\AST_UNARY_OP` * * @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 visitUnaryOp(Node $node) : UnionType { // Shortcut some easy operators switch ($node->flags) { case \ast\flags\UNARY_BOOL_NOT: return BoolType::instance()->asUnionType(); } return UnionType::fromNode($this->context, $this->code_base, $node->children['expr']); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitIfElem(Node $node) : Context { if (!isset($node->children['cond']) || !$node->children['cond'] instanceof Node) { return $this->context; } // Get the type just to make sure everything // is defined. $expression_type = UnionType::fromNode($this->context, $this->code_base, $node->children['cond']); // Look to see if any proofs we do within the condition // can say anything about types within the statement // list. return (new ConditionVisitor($this->code_base, $this->context))($node->children['cond']); }
/** * 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, FunctionInterface $method, Node $node) { $method->addReference($this->context); // Create variables for any pass-by-reference // parameters $argument_list = $node->children['args']; foreach ($argument_list->children as $i => $argument) { if (!is_object($argument)) { continue; } $parameter = $method->getParameterForCaller($i); 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 = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateVariable(); } elseif ($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 = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateProperty($argument->children['prop']); } catch (IssueException $exception) { Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); } catch (\Exception $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) { if (!is_object($argument)) { continue; } $parameter = $method->getParameterForCaller($i); if (!$parameter) { continue; } if (Config::get()->dead_code_detection) { (new ArgumentVisitor($this->code_base, $this->context))($argument); } // 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 = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateVariable(); } elseif ($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 = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateProperty($argument->children['prop']); } catch (IssueException $exception) { Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); } catch (\Exception $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->getVariadicElementUnionType()); } } } // 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(); // Create a backup of the method's scope so that we can // reset it after f*****g with it below $original_method_scope = $method->getInternalScope(); foreach ($argument_list->children as $i => $argument) { // TODO(Issue #376): Support inference on the child in **the set of vargs**, not just the first vararg // This is just testing the first vararg. // The implementer will also need to restore the original parameter list. $parameter = $original_parameter_list[$i] ?? null; if (!$parameter) { continue; } // If the parameter has no type, pass the // argument's type to it if ($parameter->getVariadicElementUnionType()->isEmpty()) { $has_argument_parameter_mismatch = true; // 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->isInternal()) { $argument_type = UnionType::fromNode($this->context, $this->code_base, $argument); // Clone the parameter in the original // parameter list so we can reset it // later // TODO: If there are varargs and this is beyond the end, ensure last arg is cloned. $original_parameter_list[$i] = clone $original_parameter_list[$i]; // 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->getVariadicElementUnionType()->addUnionType($argument_type); if (!is_object($argument)) { continue; } // If we're passing by reference, get the variable // we're dealing with wrapped up and shoved into // the scope of the method if ($parameter->isPassByReference()) { if ($original_parameter_list[$i]->isVariadic()) { // For now, give up and work on it later. // TODO(Issue #376): It's possible to have a parameter `&...$args`. Analysing that is going to be a problem. // Is it possible to create `PassByReferenceVariableCollection extends Variable` or something similar? } elseif ($argument->kind == \ast\AST_VAR) { // Get the variable $variable = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateVariable(); $pass_by_reference_variable = new PassByReferenceVariable($parameter, $variable); $parameter_list = $method->getParameterList(); $parameter_list[$i] = $pass_by_reference_variable; $method->setParameterList($parameter_list); // Add it to the scope of the function wrapped // in a way that makes it addressable as the // parameter its mimicking $method->getInternalScope()->addVariable($pass_by_reference_variable); } else { if ($argument->kind == \ast\AST_STATIC_PROP) { // Get the variable $property = (new ContextNode($this->code_base, $this->context, $argument))->getOrCreateProperty($argument->children['prop'] ?? ''); $pass_by_reference_variable = new PassByReferenceVariable($parameter, $property); $parameter_list = $method->getParameterList(); $parameter_list[$i] = $pass_by_reference_variable; $method->setParameterList($parameter_list); // Add it to the scope of the function wrapped // in a way that makes it addressable as the // parameter its mimicking $method->getInternalScope()->addVariable($pass_by_reference_variable); } } } else { // Overwrite the method's variable representation // of the parameter with the parameter with the // new type $method->getInternalScope()->addVariable($parameter); } } } } // 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->isInternal() && (!$this->context->isInFunctionLikeScope() || $method->getFQSEN() !== $this->context->getFunctionLikeFQSEN())) { $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); // Reset the scope to its original version before we // put new parameters in it $method->setInternalScope($original_method_scope); }
/** * Visit a node with kind `\ast\AST_CLASS_CONST_DECL` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitClassConstDecl(Node $node) : Context { $clazz = $this->getContextClass(); foreach ($node->children ?? [] as $child_node) { $constant = new Constant($this->context->withLineNumberStart($child_node->lineno ?? 0)->withLineNumberEnd($child_node->endLineno ?? 0), $child_node->children['name'], UnionType::fromNode($this->context, $this->code_base, $child_node->children['value']), $child_node->flags ?? 0); $clazz->addConstant($this->code_base, $constant); } return $this->context; }
/** * Visit a node with kind `\ast\AST_ARRAY` * * @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 visitArray(Node $node) : UnionType { if (!empty($node->children) && $node->children[0] instanceof Node && $node->children[0]->kind == \ast\AST_ARRAY_ELEM) { $element_types = []; // Check the first 5 (completely arbitrary) elements // and assume the rest are the same type for ($i = 0; $i < 5; $i++) { // Check to see if we're out of elements if (empty($node->children[$i])) { break; } if ($node->children[$i]->children['value'] instanceof Node) { $element_types[] = UnionType::fromNode($this->context, $this->code_base, $node->children[$i]->children['value']); } else { $element_types[] = Type::fromObject($node->children[$i]->children['value'])->asUnionType(); } } $element_types = array_values(array_unique($element_types)); if (count($element_types) == 1) { return $element_types[0]->asGenericArrayTypes(); } } return ArrayType::instance()->asUnionType(); }
/** * @return Parameter * A parameter built from a node * * @see \Phan\Deprecated\Pass1::node_param * Formerly `function node_param` */ public static function fromNode(Context $context, CodeBase $code_base, Node $node) : Parameter { assert($node instanceof Node, "node was not an \\ast\\Node"); // Get the type of the parameter $union_type = UnionType::fromNode($context, $code_base, $node->children['type']); // Create the skeleton parameter from what we know so far $parameter = new Parameter($context, (string) $node->children['name'], $union_type, $node->flags ?? 0); // If there is a default value, store it and its type if (($default_node = $node->children['default']) !== null) { // We can't figure out default values during the // parsing phase, unfortunately if (!$default_node instanceof Node || $default_node->kind == \ast\AST_CONST || $default_node->kind == \ast\AST_UNARY_OP || $default_node->kind == \ast\AST_ARRAY) { // Get the type of the default $union_type = UnionType::fromNode($context, $code_base, $default_node); // Set the default value $parameter->setDefaultValueType($union_type); } else { try { // Get the type of the default $union_type = UnionType::fromNode($context, $code_base, $default_node, false); } catch (IssueException $exception) { // If we're in the parsing phase and we // depend on a constant that isn't yet // defined, give up and set it to // bool|float|int|string to avoid having // to handle a future type. $union_type = new UnionType([BoolType::instance(), FloatType::instance(), IntType::instance(), StringType::instance()]); } // Set the default value $parameter->setDefaultValueType($union_type); } } return $parameter; }
/** * @return string * A string representation of the union type begotten from * the first statement in the statement list in the given * code. */ private function typeStringFromCode(string $code) : string { return UnionType::fromNode($this->context, $this->code_base, \ast\parse_code($code, Config::get()->ast_version)->children[0])->asExpandedTypes($this->code_base)->__toString(); }
/** * @return Parameter * A parameter built from a node * * @see \Phan\Deprecated\Pass1::node_param * Formerly `function node_param` */ public static function fromNode(Context $context, CodeBase $code_base, Node $node) : Parameter { assert($node instanceof Node, "node was not an \\ast\\Node"); // Get the type of the parameter $type = UnionType::fromSimpleNode($context, $node->children['type']); $comment = Comment::fromStringInContext($node->docComment ?? '', $context); // Create the skeleton parameter from what we know so far $parameter = new Parameter($context, (string) $node->children['name'], $type, $node->flags ?? 0); // If there is a default value, store it and its type if (($default_node = $node->children['default']) !== null) { // We can't figure out default values during the // parsing phase, unfortunately if (!$default_node instanceof Node || $default_node->kind == \ast\AST_CONST || $default_node->kind == \ast\AST_UNARY_OP || $default_node->kind == \ast\AST_ARRAY) { // Set the default value $parameter->setDefaultValue($node->children['default'], UnionType::fromNode($context, $code_base, $node->children['default'])); } else { // Nodes here may be of type \ast\AST_CLASS_CONST // which we can't figure out during the first // parsing pass $parameter->setDefaultValue(null, NullType::instance()->asUnionType()); } } return $parameter; }
/** * @param Context $context * The context in which the node appears * * @param CodeBase $code_base * * @param Node $node * An AST node representing a method * * @return Method * A Method representing the AST node in the * given context */ public static function fromNode(Context $context, CodeBase $code_base, Decl $node, FullyQualifiedMethodName $fqsen) : Method { // Create the skeleton method object from what // we know so far $method = new Method($context, (string) $node->name, new UnionType(), $node->flags ?? 0, $fqsen); // Parse the comment above the method to get // extra meta information about the method. $comment = Comment::fromStringInContext($node->docComment ?? '', $context); // @var Parameter[] // The list of parameters specified on the // method $parameter_list = Parameter::listFromNode($context, $code_base, $node->children['params']); // Add each parameter to the scope of the function foreach ($parameter_list as $parameter) { $method->getInternalScope()->addVariable($parameter); } // If the method is Analyzable, set the node so that // we can come back to it whenever we like and // rescan it $method->setNode($node); // Set the parameter list on the method $method->setParameterList($parameter_list); $method->setNumberOfRequiredParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isRequired() ? 1 : 0); }, 0)); $method->setNumberOfOptionalParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isOptional() ? 1 : 0); }, 0)); // Check to see if the comment specifies that the // method is deprecated $method->setIsDeprecated($comment->isDeprecated()); $method->setSuppressIssueList($comment->getSuppressIssueList()); if ($method->getIsMagicCall() || $method->getIsMagicCallStatic()) { $method->setNumberOfOptionalParameters(999); $method->setNumberOfRequiredParameters(0); } // Take a look at method return types if ($node->children['returnType'] !== null) { // Get the type of the parameter $union_type = UnionType::fromNode($context, $code_base, $node->children['returnType']); $method->getUnionType()->addUnionType($union_type); } if ($comment->hasReturnUnionType()) { // See if we have a return type specified in the comment $union_type = $comment->getReturnType(); if ($union_type->hasSelfType()) { // We can't actually figure out 'static' at this // point, but fill it in regardless. It will be partially // correct if ($context->isInClassScope()) { // n.b.: We're leaving the reference to self, static // or $this in the type because I'm guessing // it doesn't really matter. Apologies if it // ends up being an issue. $union_type->addUnionType($context->getClassFQSEN()->asUnionType()); } } $method->getUnionType()->addUnionType($union_type); } // Add params to local scope for user functions if (!$method->isInternal()) { $parameter_offset = 0; foreach ($method->getParameterList() as $i => $parameter) { if ($parameter->getUnionType()->isEmpty()) { // If there is no type specified in PHP, check // for a docComment with @param declarations. We // assume order in the docComment matches the // parameter order in the code if ($comment->hasParameterWithNameOrOffset($parameter->getName(), $parameter_offset)) { $comment_type = $comment->getParameterWithNameOrOffset($parameter->getName(), $parameter_offset)->getUnionType(); $parameter->getUnionType()->addUnionType($comment_type); } } // If there's a default value on the parameter, check to // see if the type of the default is cool with the // specified type. if ($parameter->hasDefaultValue()) { $default_type = $parameter->getDefaultValueType(); if (!$default_type->isEqualTo(NullType::instance()->asUnionType())) { if (!$default_type->isEqualTo(NullType::instance()->asUnionType()) && !$default_type->canCastToUnionType($parameter->getUnionType())) { Issue::maybeEmit($code_base, $context, Issue::TypeMismatchDefault, $node->lineno ?? 0, (string) $parameter->getUnionType(), $parameter->getName(), (string) $default_type); } $parameter->getUnionType()->addUnionType($default_type); } // If we have no other type info about a parameter, // just because it has a default value of null // doesn't mean that is its type. Any type can default // to null if ((string) $default_type === 'null' && !$parameter->getUnionType()->isEmpty()) { $parameter->getUnionType()->addType(NullType::instance()); } } ++$parameter_offset; } } return $method; }
/** * @param Context $context * The context in which the node appears * * @param CodeBase $code_base * * @param Node $node * An AST node representing a function * * @return Func * A Func representing the AST node in the * given context */ public static function fromNode(Context $context, CodeBase $code_base, Decl $node) : Func { // Parse the comment above the function to get // extra meta information about the function. $comment = Comment::fromStringInContext($node->docComment ?? '', $context); // @var Parameter[] // The list of parameters specified on the // function $parameter_list = Parameter::listFromNode($context, $code_base, $node->children['params']); // Add each parameter to the scope of the function foreach ($parameter_list as $parameter) { $context = $context->withScopeVariable($parameter); } // Create the skeleton function object from what // we know so far $func = new Func($context, (string) $node->name, new UnionType(), $node->flags ?? 0); // If the function is Analyzable, set the node so that // we can come back to it whenever we like and // rescan it $func->setNode($node); // Set the parameter list on the function $func->setParameterList($parameter_list); $func->setNumberOfRequiredParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isRequired() ? 1 : 0); }, 0)); $func->setNumberOfOptionalParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isOptional() ? 1 : 0); }, 0)); // Check to see if the comment specifies that the // function is deprecated $func->setIsDeprecated($comment->isDeprecated()); $func->setSuppressIssueList($comment->getSuppressIssueList()); // Take a look at function return types if ($node->children['returnType'] !== null) { // Get the type of the parameter $union_type = UnionType::fromNode($context, $code_base, $node->children['returnType']); $func->getUnionType()->addUnionType($union_type); } if ($comment->hasReturnUnionType()) { // See if we have a return type specified in the comment $union_type = $comment->getReturnType(); assert(!$union_type->hasSelfType(), "Function referencing self in {$context}"); $func->getUnionType()->addUnionType($union_type); } // Add params to local scope for user functions if (!$func->isInternal()) { $parameter_offset = 0; foreach ($func->getParameterList() as $i => $parameter) { if ($parameter->getUnionType()->isEmpty()) { // If there is no type specified in PHP, check // for a docComment with @param declarations. We // assume order in the docComment matches the // parameter order in the code if ($comment->hasParameterWithNameOrOffset($parameter->getName(), $parameter_offset)) { $comment_type = $comment->getParameterWithNameOrOffset($parameter->getName(), $parameter_offset)->getUnionType(); $parameter->getUnionType()->addUnionType($comment_type); } } // If there's a default value on the parameter, check to // see if the type of the default is cool with the // specified type. if ($parameter->hasDefaultValue()) { $default_type = $parameter->getDefaultValueType(); if (!$default_type->isEqualTo(NullType::instance()->asUnionType())) { if (!$default_type->isEqualTo(NullType::instance()->asUnionType()) && !$default_type->canCastToUnionType($parameter->getUnionType())) { Issue::maybeEmit($code_base, $context, Issue::TypeMismatchDefault, $node->lineno ?? 0, (string) $parameter->getUnionType(), $parameter->getName(), (string) $default_type); } $parameter->getUnionType()->addUnionType($default_type); } // If we have no other type info about a parameter, // just because it has a default value of null // doesn't mean that is its type. Any type can default // to null if ((string) $default_type === 'null' && !$parameter->getUnionType()->isEmpty()) { $parameter->getUnionType()->addType(NullType::instance()); } } ++$parameter_offset; } } return $func; }
/** * @param Node $node * A node to check types on * * @return UnionType * The resulting type(s) of the binary operation */ public function visitBinaryAdd(Node $node) : UnionType { $left = UnionType::fromNode($this->context, $this->code_base, $node->children['left']); $right = UnionType::fromNode($this->context, $this->code_base, $node->children['right']); // fast-track common cases if ($left->isType(IntType::instance()) && $right->isType(IntType::instance())) { return IntType::instance()->asUnionType(); } if (($left->isType(IntType::instance()) || $left->isType(FloatType::instance())) && ($right->isType(IntType::instance()) || $right->isType(FloatType::instance()))) { return FloatType::instance()->asUnionType(); } $left_is_array = !empty($left->genericArrayElementTypes()) && empty($left->nonGenericArrayTypes()); $right_is_array = !empty($right->genericArrayElementTypes()) && empty($right->nonGenericArrayTypes()); if ($left_is_array && !$right->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::ETYPE, "invalid operator: left operand is array and right is not", $this->context->getFile(), $node->lineno); return new UnionType(); } else { if ($right_is_array && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::ETYPE, "invalid operator: right operand is array and left is not", $this->context->getFile(), $node->lineno); return new UnionType(); } else { if ($left_is_array || $right_is_array) { // If it is a '+' and we know one side is an array // and the other is unknown, assume array return ArrayType::instance()->asUnionType(); } } } return new UnionType([IntType::instance(), FloatType::instance()]); }
/** * @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 { // Get the type returned by the first method // call. $union_type = UnionType::fromNode($this->context, $this->code_base, $node); // 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(); }
/** * @param Node $node * A node to check types on * * @return UnionType * The resulting type(s) of the binary operation */ public function visitBinaryAdd(Node $node) : UnionType { $left = UnionType::fromNode($this->context, $this->code_base, $node->children['left']); $right = UnionType::fromNode($this->context, $this->code_base, $node->children['right']); // fast-track common cases if ($left->isType(IntType::instance()) && $right->isType(IntType::instance())) { return IntType::instance()->asUnionType(); } // If both left and right are arrays, then this is array // concatenation. if ($left->isGenericArray() && $right->isGenericArray()) { if ($left->isEqualTo($right)) { return $left; } return ArrayType::instance()->asUnionType(); } if (($left->isType(IntType::instance()) || $left->isType(FloatType::instance())) && ($right->isType(IntType::instance()) || $right->isType(FloatType::instance()))) { return FloatType::instance()->asUnionType(); } $left_is_array = !empty($left->genericArrayElementTypes()) && empty($left->nonGenericArrayTypes()) || $left->isType(ArrayType::instance()); $right_is_array = !empty($right->genericArrayElementTypes()) && empty($right->nonGenericArrayTypes()) || $right->isType(ArrayType::instance()); if ($left_is_array && !$right->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::maybeEmit($this->code_base, $this->context, Issue::TypeInvalidRightOperand, $node->lineno ?? 0); return new UnionType(); } elseif ($right_is_array && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::maybeEmit($this->code_base, $this->context, Issue::TypeInvalidLeftOperand, $node->lineno ?? 0); return new UnionType(); } elseif ($left_is_array || $right_is_array) { // If it is a '+' and we know one side is an array // and the other is unknown, assume array return ArrayType::instance()->asUnionType(); } return new UnionType([IntType::instance(), FloatType::instance()]); }
/** * Visit a node with kind `\ast\AST_PROP_DECL` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitPropDecl(Node $node) : Context { // Bomb out if we're not in a class context $clazz = $this->getContextClass(); // Get a comment on the property declaration $comment = Comment::fromStringInContext($node->children[0]->docComment ?? '', $this->context); foreach ($node->children ?? [] as $i => $child_node) { // Ignore children which are not property elements if (!$child_node || $child_node->kind != \ast\AST_PROP_ELEM) { continue; } // Get the type of the default $union_type = UnionType::fromNode($this->context, $this->code_base, $child_node->children['default']); $property_name = $child_node->children['name']; assert(is_string($property_name), 'Property name must be a string. ' . 'Got ' . print_r($property_name, true) . ' at ' . $this->context); $property = new Property($this->context->withLineNumberStart($child_node->lineno ?? 0)->withLineNumberEnd($child_node->endLineno ?? -1), is_string($child_node->children['name']) ? $child_node->children['name'] : '_error_', $union_type, $node->flags ?? 0); // Add the property to the class $clazz->addProperty($this->code_base, $property); // Look for any @var declarations if ($variable = $comment->getVariableList()[$i] ?? null) { if ((string) $union_type != 'null' && !$union_type->canCastToUnionType($variable->getUnionType())) { Log::err(Log::ETYPE, "assigning {$union_type} to property but {$property->getFQSEN()} is {$variable->getUnionType()}", $this->context->getFile(), $child_node->lineno); } // Set the declared type to the doc-comment type and add // |null if the default value is null $property->getUnionType()->addUnionType($variable->getUnionType()); } } return $this->context; }