/** * @return Func[] * One or more (alternate) methods begotten from * reflection info and internal method data */ public static function functionListFromSignature(CodeBase $code_base, FullyQualifiedFunctionName $fqsen, array $signature) : array { $context = new Context(); $return_type = UnionType::fromStringInContext(array_shift($signature), $context); $func = new Func($context, $fqsen->getName(), $return_type, 0); $func->setFQSEN($fqsen); return self::functionListFromFunction($func, $code_base); }
/** * @return Func[] * One or more (alternate) methods begotten from * reflection info and internal method data */ public static function functionListFromReflectionFunction(CodeBase $code_base, \ReflectionFunction $reflection_function) : array { $context = new Context(); $parts = explode('\\', $reflection_function->getName()); $method_name = array_pop($parts); $namespace = '\\' . implode('\\', $parts); $fqsen = FullyQualifiedFunctionName::make($namespace, $method_name); $function = new Func($context, $fqsen->getName(), new UnionType(), 0, $fqsen); $function->setNumberOfRequiredParameters($reflection_function->getNumberOfRequiredParameters()); $function->setNumberOfOptionalParameters($reflection_function->getNumberOfParameters() - $reflection_function->getNumberOfRequiredParameters()); return self::functionListFromFunction($function, $code_base); }
/** * 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; // Hunt for an un-taken alternate ID $alternate_id = 0; $function_fqsen = null; do { $function_fqsen = FullyQualifiedFunctionName::fromStringInContext($function_name, $this->context)->withNamespace($this->context->getNamespace())->withAlternateId($alternate_id++); } while ($this->code_base->hasFunctionWithFQSEN($function_fqsen)); $func = Func::fromNode($this->context->withLineNumberStart($node->lineno ?? 0)->withLineNumberEnd($node->endLineno ?? 0), $this->code_base, $node); $func->setFQSEN($function_fqsen); $this->code_base->addFunction($func); // Send the context into the function and reset the scope $context = $this->context->withMethodFQSEN($function_fqsen)->withScope(new Scope()); // Add each method parameter to the scope. We clone it // so that changes to the variable don't alter the // parameter definition foreach ($func->getParameterList() as $parameter) { $context->addScopeVariable(clone $parameter); } return $context; }
/** * 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; // Hunt for an un-taken alternate ID $alternate_id = 0; $function_fqsen = null; do { $function_fqsen = FullyQualifiedFunctionName::fromStringInContext($function_name, $this->context)->withNamespace($this->context->getNamespace())->withAlternateId($alternate_id++); } while ($this->code_base->hasFunctionWithFQSEN($function_fqsen)); $func = Func::fromNode($this->context->withLineNumberStart($node->lineno ?? 0)->withLineNumberEnd($node->endLineno ?? 0), $this->code_base, $node, $function_fqsen); $this->code_base->addFunction($func); // Send the context into the function and reset the scope $context = $this->context->withScope($func->getInternalScope()); return $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 Func $function * A function to add to the code base * * @return void */ public function addFunction(Func $function) { // Add it to the map of functions $this->fqsen_func_map[$function->getFQSEN()] = $function; // Add it to the set of functions and methods $this->func_and_method_set->attach($function); }
/** * Visit a node with kind `\ast\AST_CLOSURE` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitClosure(Decl $node) : Context { $closure_fqsen = FullyQualifiedFunctionName::fromClosureInContext($this->context->withLineNumberStart($node->lineno ?? 0)); $func = Func::fromNode($this->context, $this->code_base, $node, $closure_fqsen); // If we have a 'this' variable in our current scope, // pass it down into the closure if ($this->context->getScope()->hasVariableWithName('this')) { $func->getInternalScope()->addVariable($this->context->getScope()->getVariableByName('this')); } // Make the closure reachable by FQSEN from anywhere $this->code_base->addFunction($func); if (!empty($node->children['uses']) && $node->children['uses']->kind == \ast\AST_CLOSURE_USES) { $uses = $node->children['uses']; foreach ($uses->children as $use) { if ($use->kind != \ast\AST_CLOSURE_VAR) { $this->emitIssue(Issue::VariableUseClause, $node->lineno ?? 0); continue; } $variable_name = (new ContextNode($this->code_base, $this->context, $use->children['name']))->getVariableName(); if (empty($variable_name)) { continue; } $variable = null; // Check to see if the variable exists in this scope if (!$this->context->getScope()->hasVariableWithName($variable_name)) { // If this is not pass-by-reference variable we // have a problem if (!($use->flags & \ast\flags\PARAM_REF)) { $this->emitIssue(Issue::UndeclaredVariable, $node->lineno ?? 0, $variable_name); continue; } else { // If the variable doesn't exist, but its // a pass-by-reference variable, we can // just create it $variable = Variable::fromNodeInContext($use, $this->context, $this->code_base, false); } } else { $variable = $this->context->getScope()->getVariableByName($variable_name); // If this isn't a pass-by-reference variable, we // clone the variable so state within this scope // doesn't update the outer scope if (!($use->flags & \ast\flags\PARAM_REF)) { $variable = clone $variable; } } // Pass the variable into a new scope $func->getInternalScope()->addVariable($variable); } } // Add all parameters to the scope if (!empty($node->children['params']) && $node->children['params']->kind == \ast\AST_PARAM_LIST) { $params = $node->children['params']; foreach ($params->children as $param) { // Read the parameter $parameter = Parameter::fromNode($this->context, $this->code_base, $param); // Add it to the scope $func->getInternalScope()->addVariable($parameter); } } if ($this->analyzeFunctionLikeIsGenerator($node)) { $this->setReturnTypeOfGenerator($func, $node); } return $this->context->withScope($func->getInternalScope()); }
/** * @param CodeBase $code_base * The code base in which the function exists * * @param Func $function * A function being analyzed * * @return void */ public function analyzeFunction(CodeBase $code_base, Func $function) { // As an example, we test to see if the name of the // function is `function`, and emit an issue if it is. if ($function->getName() == 'function') { $this->emitIssue($code_base, $function->getContext(), 'DemoPluginFunctionName', "Function {$function->getFQSEN()} cannot be called `function`"); } }