/** * 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 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 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; }
/** * @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; }
/** * Visit a node with kind `\ast\AST_FUNC_DECL` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitFuncDecl(Decl $node) : Context { $function_name = (string) $node->name; try { $canonical_function = (new ContextNode($this->code_base, $this->context, $node))->getFunction($function_name, true); } catch (CodeBaseException $exception) { // This really ought not happen given that // we already successfully parsed the code // base throw $exception; return $this->context; } // Hunt for the alternate associated with the file we're // looking at currently in this context. $function = null; foreach ($canonical_function->alternateGenerator($this->code_base) as $i => $alternate_function) { if ($alternate_function->getFileRef()->getProjectRelativePath() === $this->context->getProjectRelativePath()) { $function = $alternate_function; break; } } if (empty($function)) { // No alternate was found throw new CodeBaseException(null, "Can't find function {$function_name} in context {$this->context} - aborting"); } $context = $this->context->withScope($function->getInternalScope()); // Parse the comment above the method to get // extra meta information about the method. $comment = Comment::fromStringInContext($node->docComment ?? '', $this->context); // For any @var references in the method declaration, // add them as variables to the method's scope foreach ($comment->getVariableList() as $parameter) { $context->addScopeVariable($parameter->asVariable($this->context)); } // Add each method parameter to the scope. We clone it // so that changes to the variable don't alter the // parameter definition foreach ($function->getParameterList() as $parameter) { $context->addScopeVariable(clone $parameter); } if ($this->analyzeFunctionLikeIsGenerator($node)) { $this->setReturnTypeOfGenerator($function, $node); } return $context; }
/** * Visit a node with kind `\ast\AST_METHOD` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitMethod(Decl $node) : Context { $method_name = (string) $node->name; $clazz = $this->getContextClass(); if (!$clazz->hasMethodWithName($this->code_base, $method_name)) { throw new CodeBaseException(null, "Can't find method {$clazz->getFQSEN()}::{$method_name}() - aborting"); } $method = $clazz->getMethodByNameInContext($this->code_base, $method_name, $this->context); // Parse the comment above the method to get // extra meta information about the method. $comment = Comment::fromStringInContext($node->docComment ?? '', $this->context); // For any @var references in the method declaration, // add them as variables to the method's scope foreach ($comment->getVariableList() as $parameter) { $method->getContext()->addScopeVariable($parameter->asVariable($this->context)); } // Add $this to the scope of non-static methods if (!($node->flags & \ast\flags\MODIFIER_STATIC)) { assert($clazz->getContext()->getScope()->hasVariableWithName('this'), "Classes must have a \$this variable."); $method->getContext()->addScopeVariable($clazz->getContext()->getScope()->getVariableWithName('this')); } // Add each method parameter to the scope. We clone it // so that changes to the variable don't alter the // parameter definition foreach ($method->getParameterList() as $parameter) { $method->getContext()->addScopeVariable(clone $parameter); } return $method->getContext()->withMethodFQSEN($method->getFQSEN()); }
/** * @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 * * * @see \Phan\Deprecated\Pass1::node_func * Formerly 'function node_func' */ public static function fromNode(Context $context, CodeBase $code_base, Node $node) : Method { // 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) { $context = $context->withScopeVariable($parameter); } // Create the skeleton method object from what // we know so far $method = new Method($context, $node->name, new UnionType(), $node->flags ?? 0); // 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()); // Take a look at method return types if ($node->children['returnType'] !== null) { $union_type = UnionType::fromSimpleNode($context, $node->children['returnType']); $method->getUnionType()->addUnionType($union_type); } else { 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->hasClassFQSEN()) { $union_type = $union_type->addUnionType($context->getClassFQSEN()->asUnionType()); } } $method->getUnionType()->addUnionType($union_type); } } // Add params to local scope for user functions if ($context->getFile() != 'internal') { $parameter_offset = 0; foreach ($method->parameter_list 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->hasParameterAtOffset($parameter_offset)) { $comment_type = $comment->getParameterAtOffset($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->canCastToUnionType($parameter->getUnionType())) { Log::err(Log::ETYPE, "Default value for {$parameter->getUnionType()} \${$parameter->getName()} can't be {$default_type}", $context->getFile(), $node->lineno); } // 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; }
/** * @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"); // 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; // Get the type of the parameter $union_type = UnionType::fromNode($context, $code_base, $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'], $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; }