/** * @param Context $context * The context in which the structural element lives * * @param string $name, * The name of the typed structural element * * @param UnionType $type, * A '|' delimited set of types satisfyped by this * typed structural element. * * @param int $flags, * The flags property contains node specific flags. It is * always defined, but for most nodes it is always zero. * ast\kind_uses_flags() can be used to determine whether * a certain kind has a meaningful flags value. */ public function __construct(Context $context, string $name, UnionType $type, int $flags) { $this->context = $context; $this->name = $name; $this->type = $type; $this->flags = $flags; $this->setIsInternal($context->isInternal()); }
public function testSimple() { $context = new Context(); $context_namespace = $context->withNamespace('\\A'); $context_class = $context_namespace->withScope(new ClassScope($context_namespace->getScope(), FullyQualifiedClassName::fromFullyQualifiedString('\\A\\B'))); $context_method = $context_namespace->withScope(new FunctionLikeScope($context_namespace->getScope(), FullyQualifiedMethodName::fromFullyQualifiedString('\\A\\b::c'))); $this->assertTrue(!empty($context)); $this->assertTrue(!empty($context_namespace)); $this->assertTrue(!empty($context_class)); $this->assertTrue(!empty($context_method)); }
public function testSimple() { $context = new Context(); $context_namespace = $context->withNamespace('\\A'); $context_class = $context_namespace->withClassFQSEN(FullyQualifiedClassName::fromFullyQualifiedString('\\A\\B')); $context_method = $context_namespace->withMethodFQSEN(FullyQualifiedMethodName::fromFullyQualifiedString('\\A\\b::c')); $this->assertTrue(!empty($context)); $this->assertTrue(!empty($context_namespace)); $this->assertTrue(!empty($context_class)); $this->assertTrue(!empty($context_method)); }
/** * @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()); $right_is_array = !empty($right->genericArrayElementTypes()) && empty($right->nonGenericArrayTypes()); if ($left_is_array && !$right->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::emit(Issue::TypeInvalidRightOperand, $this->context->getFile(), $node->lineno ?? 0); return new UnionType(); } elseif ($right_is_array && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::emit(Issue::TypeInvalidLeftOperand, $this->context->getFile(), $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()]); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitVar(Node $node) : Context { $variable_name = AST::variableName($node); // Check to see if the variable already exists if ($this->context->getScope()->hasVariableWithName($variable_name)) { $variable = $this->context->getScope()->getVariableWithName($variable_name); // If we're assigning to an array element then we don't // know what the constitutation of the parameter is // outside of the scope of this assignment, so we add to // its union type rather than replace it. if ($this->is_dim_assignment) { $variable->getUnionType()->addUnionType($this->right_type); } else { // If the variable isn't a pass-by-reference paramter // we clone it so as to not disturb its previous types // as we replace it. if (!($variable instanceof Parameter && $variable->isPassByReference())) { $variable = clone $variable; } $variable->setUnionType($this->right_type); } $this->context->addScopeVariable($variable); return $this->context; } $variable = Variable::fromNodeInContext($this->assignment_node, $this->context, $this->code_base); // Set that type on the variable $variable->getUnionType()->addUnionType($this->right_type); // Note that we're not creating a new scope, just // adding variables to the existing scope $this->context->addScopeVariable($variable); return $this->context; }
/** * @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 ''; }
/** * Perform some backwards compatibility checks on a node * * @return void */ public function analyzeBackwardCompatibility() { if (!Config::get()->backward_compatibility_checks) { return; } if (empty($this->node->children['expr'])) { return; } if ($this->node->kind === \ast\AST_STATIC_CALL || $this->node->kind === \ast\AST_METHOD_CALL) { return; } $llnode = $this->node; if ($this->node->kind !== \ast\AST_DIM) { if (!$this->node->children['expr'] instanceof Node) { return; } if ($this->node->children['expr']->kind !== \ast\AST_DIM) { (new ContextNode($this->code_base, $this->context, $this->node->children['expr']))->analyzeBackwardCompatibility(); return; } $temp = $this->node->children['expr']->children['expr']; $llnode = $this->node->children['expr']; $lnode = $temp; } else { $temp = $this->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)) { $llnode = $lnode; $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 && $this->node->kind !== \ast\AST_CALL) { return; } // $this->$bar['baz']; is a problem // $this->bar['baz'] is not if ($lnode->kind === \ast\AST_PROP && !$lnode->children['prop'] instanceof Node && !$llnode->children['prop'] instanceof Node) { 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) || !empty($lnode->children['expr']) && $lnode->children['expr'] instanceof Node && ($lnode->children['expr']->kind == \ast\AST_VAR || $lnode->children['expr']->kind == \ast\AST_NAME)) && ($temp->kind == \ast\AST_VAR || $temp->kind == \ast\AST_NAME)) { $ftemp = new \SplFileObject($this->context->getFile()); $ftemp->seek($this->node->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '}[') === false || strpos($line, ']}') === false || strpos($line, '>{') === false) { Issue::maybeEmit($this->code_base, $this->context, Issue::CompatiblePHP7, $this->node->lineno ?? 0); } } }
/** * @param Context $context * The context in which the FQSEN string was found * * @param $fqsen_string * An FQSEN string like '\Namespace\Class' */ public static function fromStringInContext(string $fqsen_string, Context $context) : FullyQualifiedGlobalStructuralElement { // Check to see if we're fully qualified if (0 === strpos($fqsen_string, '\\')) { return static::fromFullyQualifiedString($fqsen_string); } // Split off the alternate ID $parts = explode(',', $fqsen_string); $fqsen_string = $parts[0]; $alternate_id = (int) ($parts[1] ?? 0); assert(is_int($alternate_id), "Alternate must be an integer in {$fqsen_string}"); $parts = explode('\\', $fqsen_string); $name = array_pop($parts); assert(!empty($name), "The name cannot be empty in {$fqsen_string}"); // Check for a name map if ($context->hasNamespaceMapFor(static::getNamespaceMapType(), $name)) { return $context->getNamespaceMapFor(static::getNamespaceMapType(), $name); } $namespace = implode('\\', array_filter($parts)); // n.b.: Functions must override this method because // they don't prefix the namespace for naked // calls if (empty($namespace)) { $namespace = $context->getNamespace(); } return static::make($namespace, $name, $alternate_id); }
/** * 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(); }
/** * @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 to parse * * @return void */ public function visitClosure(Decl $node) { try { $method = (new ContextNode($this->code_base, $this->context->withLineNumberStart($node->lineno ?? 0), $node))->getClosure(); $method->addReference($this->context); } catch (\Exception $exception) { // Swallow it } }
/** * @return bool * False if the class name doesn't point to a known class */ private function classExistsOrIsNative(Node $node) : bool { if ($this->classExists()) { return true; } $type = UnionType::fromStringInContext($this->class_name, $this->context); if ($type->isNativeType()) { return true; } Log::err(Log::EUNDEF, "reference to undeclared class {$this->class_fqsen}", $this->context->getFile(), $node->lineno); return false; }
/** * @param Node $node * A node to check types on * * @return UnionType * The resulting type(s) of the binary operation */ private function visitBinaryOpCommon(Node $node) { $left = UnionType::fromNode($this->context, $this->code_base, $node->children['left']); $right = UnionType::fromNode($this->context, $this->code_base, $node->children['right']); if (!$left->asNonGenericTypes()->isEmpty() && $left->nonGenericTypes()->isEmpty() && !$right->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::ETYPE, "array to {$right} comparison", $this->context->getFile(), $node->lineno); } else { if (!$right->asNonGenericTypes()->isEmpty() && $right->nonGenericTypes()->isEmpty() && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { // and the same for the right side Log::err(Log::ETYPE, "{$left} to array comparison", $this->context->getFile(), $node->lineno); } } return BoolType::instance()->asUnionType(); }
/** * @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 ''; } // 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 AST::qualifiedName($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 ''; }
/** * @return Clazz[] * A list of classes associated with the given node * * @throws IssueException * An exception is thrown if we can't find a class for * the given type */ private function classListFromNode(Node $node) { // Get the types associated with the node $union_type = self::unionTypeFromNode($this->code_base, $this->context, $node); // Iterate over each viable class type to see if any // have the constant we're looking for foreach ($union_type->nonNativeTypes()->getTypeSet() as $class_type) { // Get the class FQSEN $class_fqsen = $class_type->asFQSEN(); // See if the class exists if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) { throw new IssueException(Issue::fromType(Issue::UndeclaredClassReference)($this->context->getFile(), $node->lineno ?? 0, [(string) $class_fqsen])); } (yield $this->code_base->getClassByFQSEN($class_fqsen)); } }
/** * @param Context $context * The context in which the FQSEN string was found * * @param $fqsen_string * An FQSEN string like '\Namespace\Class::methodName' * * @return FullyQualifiedMethodName */ public static function fromStringInContext(string $fqsen_string, Context $context) { // Test to see if we have a class defined if (false === strpos($fqsen_string, '::')) { $fully_qualified_class_name = $context->getClassFQSEN(); } else { assert(false !== strpos($fqsen_string, '::'), "Fully qualified class element lacks '::' delimiter in {$fqsen_string}."); list($class_name_string, $fqsen_string) = explode('::', $fqsen_string); $fully_qualified_class_name = FullyQualifiedClassName::fromStringInContext($class_name_string, $context); } // Split off the alternate ID $parts = explode(',', $fqsen_string); $name = $parts[0]; $alternate_id = (int) ($parts[1] ?? 0); assert(is_int($alternate_id), "Alternate must be an integer in {$fqsen_string}"); return static::make($fully_qualified_class_name, $name, $alternate_id); }
/** * @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; }
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 = (new ContextNode($this->code_base, $this->context, $node))->getUnqualifiedNameForAnonymousClass(); // Turn that into a fully qualified name $fqsen = FullyQualifiedClassName::fromStringInContext($anonymous_class_name, $this->context); // Turn that into a union type return Type::fromFullyQualifiedString((string) $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 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 (new ContextNode($this->code_base, $this->context, $node->children['class']))->getUnqualifiedNameForAnonymousClass(); } // 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 ''; }
/** * @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; } 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 $this->context->addScopeVariable($variable); } catch (\Exception $exception) { // Swallow it } return $this->context; }
/** * Visit a node with kind `\ast\AST_NAMESPACE` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitNamespace(Node $node) : Context { $namespace = '\\' . (string) $node->children['name']; return $this->context->withNamespace($namespace); }
/** * @param string $name * The name of the property * * @param Context $context * The context of the caller requesting the property * * @return Property * A property with the given name * * @throws AccessException * An exception may be thrown if the caller does not * have access to the given property from the given * context */ public function getPropertyByNameInContext(CodeBase $code_base, string $name, Context $context) : Property { $property = $code_base->getProperty($this->getFQSEN(), $name); // If we're getting the property from outside of this // class and the property isn't public and we don't // have a getter or setter, emit an access error if ((!$context->hasClassFQSEN() || $context->getClassFQSEN() != $this->getFQSEN()) && !$property->isPublic() && !$this->hasMethodWithName($code_base, '__get') && !$this->hasMethodWithName($code_base, '__set')) { if ($property->isPrivate()) { throw new AccessException("Cannot access private property {$this->getFQSEN()}::\${$property->getName()}"); } if ($property->isProtected()) { throw new AccessException("Cannot access protected property {$this->getFQSEN()}::\${$property->getName()}"); } } return $property; }
/** * @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); } } } }
/** * @param CodeBase $code_base * A code base needs to be passed in because we require * it to be initialized before any classes or files are * loaded. * * @param Context $context * The context in which this node exists * * @param Node $node * A node to parse and scan for errors * * @return Context * The context from within the node is returned */ public static function analyzeNodeInContext(CodeBase $code_base, Context $context, Node $node, Node $parent_node = null, int $depth = 0) : Context { // Visit the given node populating the code base // with anything we learn and get a new context // indicating the state of the world within the // given node $node_context = (new PreOrderAnalysisVisitor($code_base, $context->withLineNumberStart($node->lineno ?? 0)))($node); assert(!empty($context), 'Context cannot be null'); // We collect all child context so that the // PostOrderAnalysisVisitor can optionally operate on // them $child_context_list = []; $child_context = $node_context; // With a context that is inside of the node passed // to this method, we analyze all children of the // node. foreach ($node->children ?? [] as $child_node) { // Skip any non Node children. if (!$child_node instanceof Node) { continue; } if (!self::shouldVisit($child_node)) { $child_context->withLineNumberStart($child_node->lineno ?? 0); continue; } // All nodes but conditionals pass context to // their siblings. Child nodes of conditionals // operate in a context independent of eachother switch ($child_node->kind) { case \ast\AST_IF_ELEM: $child_context = $node_context; break; } // Step into each child node and get an // updated context for the node $child_context = self::analyzeNodeInContext($code_base, $child_context->withLineNumberStart($child_node->lineno ?? 0), $child_node, $node, $depth + 1); $child_context_list[] = $child_context; } // For if statements, we need to merge the contexts // of all child context into a single scope based // on any possible branching structure $node_context = (new ContextMergeVisitor($code_base, $node_context, $child_context_list))($node); // Now that we know all about our context (like what // 'self' means), we can analyze statements like // assignments and method calls. $node_context = (new PostOrderAnalysisVisitor($code_base, $node_context->withLineNumberStart($node->lineno ?? 0), $parent_node))($node); // When coming out of a scoped element, we pop the // context to be the incoming context. Otherwise, // we pass our new context up to our parent switch ($node->kind) { case \ast\AST_CLASS: case \ast\AST_METHOD: case \ast\AST_FUNC_DECL: case \ast\AST_CLOSURE: return $context; default: return $node_context; } }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitIf(Node $node) : Context { // Get the list of scopes for each branch of the // conditional $scope_list = array_map(function (Context $context) { return $context->getScope(); }, $this->child_context_list); $has_else = array_reduce($node->children ?? [], function (bool $carry, $child_node) { return $carry || $child_node instanceof Node && empty($child_node->children['cond']); }, false); // If we're not guaranteed to hit at least one // branch, mark the incoming scope as a possibility if (!$has_else) { $scope_list[] = $this->context->getScope(); } // If there weren't multiple branches, continue on // as if the conditional never happened if (count($scope_list) < 2) { return array_values($this->child_context_list)[0]; } // Get a list of all variables in all scopes $variable_map = []; foreach ($scope_list as $scope) { foreach ($scope->getVariableMap() as $name => $variable) { $variable_map[$name] = $variable; } } // A function that determins if a variable is defined on // every branch $is_defined_on_all_branches = function (string $variable_name) use($scope_list) { return array_reduce($scope_list, function (bool $has_variable, Scope $scope) use($variable_name) { return $has_variable && $scope->hasVariableWithName($variable_name); }, true); }; // Get the intersection of all types for all versions of // the variable from every side of the branch $common_union_type = function (string $variable_name) use($scope_list) { // Get a list of all variables with the given name from // each scope $variable_list = array_filter(array_map(function (Scope $scope) use($variable_name) { if (!$scope->hasVariableWithName($variable_name)) { return null; } return $scope->getVariableWithName($variable_name); }, $scope_list)); // Get the list of types for each version of the variable $type_set_list = array_map(function (Variable $variable) : Set { return $variable->getUnionType()->getTypeSet(); }, $variable_list); if (count($type_set_list) < 2) { return new UnionType($type_set_list[0] ?? []); } return new UnionType(Set::intersectAll($type_set_list)); }; $scope = new Scope(); foreach ($variable_map as $name => $variable) { // Skip variables that are only partially defined if (!$is_defined_on_all_branches($name)) { continue; } // Limit the type of the variable to the subset // of types that are common to all branches $variable = clone $variable; $variable->setUnionType($common_union_type($name)); // Add the variable to the outgoing scope $scope->addVariable($variable); } // print '<'.implode("\t", $scope_list) . "\n"; // print '>'.$scope."\n"; // Set the new scope with only the variables and types // that are common to all branches return $this->context->withScope($scope); }
/** * @param CodeBase $code_base * The code base within which we're operating * * @param Context $context * The context in which the node we're going to be looking * at exits. * * @param string $issue_type * The type of issue to emit such as Issue::ParentlessClass * * @param int $lineno * The line number where the issue was found * * @param array parameters * Template parameters for the issue's error message * * @return void */ public static function maybeEmitWithParameters(CodeBase $code_base, Context $context, string $issue_type, int $lineno, array $parameters) { // If this issue type has been suppressed in // the config, ignore it if (in_array($issue_type, Config::get()->suppress_issue_types ?? [])) { return; } if ($context->hasSuppressIssue($code_base, $issue_type)) { return; } Issue::emitWithParameters($issue_type, $context->getFile(), $lineno, $parameters); }
/** * @param Node $node * A node to check to see if its a no-op * * @param string $message * A message to emit if its a no-op * * @return null */ private function analyzeNoOp(Node $node, string $message) { if ($this->parent_node instanceof Node && $this->parent_node->kind == \ast\AST_STMT_LIST) { Log::err(Log::ENOOP, $message, $this->context->getFile(), $node->lineno); } }
/** * @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) { Issue::emit(Issue::TypeNonVarPassByRef, $context->getFile(), $node->lineno ?? 0, $i + 1, (string) $method->getFQSEN()); } else { $variable_name = (new ContextNode($code_base, $context, $argument))->getVariableName(); if ($argument->kind == \ast\AST_STATIC_PROP) { if (in_array($variable_name, ['self', 'static', 'parent'])) { Issue::emit(Issue::ContextNotObject, $context->getFile(), $node->lineno ?? 0, "\${$variable_name}"); } } } } // 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); // 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()) { Issue::emit(Issue::TypeMismatchArgumentInternal, $context->getFile(), $node->lineno ?? 0, $i + 1, $parameter_name, $argument_type_expanded, (string) $method->getFQSEN(), (string) $parameter_type); } else { Issue::emit(Issue::TypeMismatchArgument, $context->getFile(), $node->lineno ?? 0, $i + 1, $parameter_name, $argument_type_expanded, (string) $method->getFQSEN(), (string) $parameter_type, $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()); } } } }
/** * @param string $string * A string representing a type * * @param Context $context * The context in which the type string was * found * * @return Type * Parse a type from the given string */ public static function fromStringInContext(string $string, Context $context) : Type { assert($string !== '', "Type cannot be empty in {$context}"); $namespace = null; // Extract the namespace if the type string is // fully-qualified if ('\\' === $string[0]) { list($namespace, $string) = self::namespaceAndTypeFromString($string); } $type_name = $string; // @var bool // True if this type name if of the form 'C[]' $is_generic_array_type = self::isGenericArrayString($type_name); // If this is a generic array type, get the name of // the type of each element $non_generic_array_type_name = $type_name; if ($is_generic_array_type && false !== ($pos = strpos($type_name, '[]'))) { $non_generic_array_type_name = substr($type_name, 0, $pos); } // Check to see if the type name is mapped via // a using clause. // // Gotta check this before checking for native types // because there are monsters out there that will // remap the names via things like `use \Foo\String`. if ($context->hasNamespaceMapFor(T_CLASS, $non_generic_array_type_name)) { $fqsen = $context->getNamespaceMapFor(T_CLASS, $non_generic_array_type_name); if ($is_generic_array_type) { return GenericArrayType::fromElementType(Type::make($fqsen->getNamespace(), $fqsen->getName())); } return Type::make($fqsen->getNamespace(), $fqsen->getName()); } // If this was a fully qualified type, we're all // set if (!empty($namespace)) { return self::fromNamespaceAndName($namespace, $type_name); } if ($is_generic_array_type && self::isNativeTypeString($type_name)) { return self::fromInternalTypeName($type_name); } else { // Check to see if its a builtin type switch (self::canonicalNameFromName($type_name)) { case 'array': return \Phan\Language\Type\ArrayType::instance(); case 'bool': return \Phan\Language\Type\BoolType::instance(); case 'callable': return \Phan\Language\Type\CallableType::instance(); case 'float': return \Phan\Language\Type\FloatType::instance(); case 'int': return \Phan\Language\Type\IntType::instance(); case 'mixed': return \Phan\Language\Type\MixedType::instance(); case 'null': return \Phan\Language\Type\NullType::instance(); case 'object': return \Phan\Language\Type\ObjectType::instance(); case 'resource': return \Phan\Language\Type\ResourceType::instance(); case 'string': return \Phan\Language\Type\StringType::instance(); case 'void': return \Phan\Language\Type\VoidType::instance(); } } // Things like `self[]` or `$this[]` if ($is_generic_array_type && self::isSelfTypeString($non_generic_array_type_name) && $context->isInClassScope()) { // Callers of this method should be checking on their own // to see if this type is a reference to 'parent' and // dealing with it there. We don't want to have this // method be dependent on the code base assert('parent' !== $non_generic_array_type_name, __METHOD__ . " does not know how to handle the type name 'parent' in {$context}"); return GenericArrayType::fromElementType(static::fromFullyQualifiedString((string) $context->getClassFQSEN())); } // If this is a type referencing the current class // in scope such as 'self' or 'static', return that. if (self::isSelfTypeString($type_name) && $context->isInClassScope()) { // Callers of this method should be checking on their own // to see if this type is a reference to 'parent' and // dealing with it there. We don't want to have this // method be dependent on the code base assert('parent' !== $type_name, __METHOD__ . " does not know how to handle the type name 'parent' in {$context}"); return static::fromFullyQualifiedString((string) $context->getClassFQSEN()); } // Attach the context's namespace to the type name return self::fromNamespaceAndName($context->getNamespace() ?: '\\', $type_name); }
/** * @param string $string * A string representing a type * * @param Context $context * The context in which the type string was * found * * @return Type * Parse a type from the given string */ public static function fromStringInContext(string $string, Context $context) : Type { assert($string !== '', "Type cannot be empty in {$context}"); $namespace = null; // Extract the namespace if the type string is // fully-qualified if ('\\' === $string[0]) { list($namespace, $string) = self::namespaceAndTypeFromString($string); } $type_name = strtolower($string); // Check to see if the type name is mapped via // a using clause. // // Gotta check this before checking for native types // because there are monsters out there that will // remap the names via things like `use \Foo\String`. if ($context->hasNamespaceMapFor(T_CLASS, $type_name)) { $fqsen = $context->getNamespaceMapFor(T_CLASS, $type_name); return new Type($fqsen->getNamespace(), $fqsen->getName()); } // If this was a fully qualified type, we're all // set if (!empty($namespace)) { return self::fromNamespaceAndName($namespace, $type_name); } // Check to see if its a builtin type switch (self::canonicalNameFromName($type_name)) { case 'array': return \Phan\Language\Type\ArrayType::instance(); case 'bool': return \Phan\Language\Type\BoolType::instance(); case 'callable': return \Phan\Language\Type\CallableType::instance(); case 'float': return \Phan\Language\Type\FloatType::instance(); case 'int': return \Phan\Language\Type\IntType::instance(); case 'mixed': return \Phan\Language\Type\MixedType::instance(); case 'null': return \Phan\Language\Type\NullType::instance(); case 'object': return \Phan\Language\Type\ObjectType::instance(); case 'resource': return \Phan\Language\Type\ResourceType::instance(); case 'string': return \Phan\Language\Type\StringType::instance(); case 'void': return \Phan\Language\Type\VoidType::instance(); } // If this is a type referencing the current class // in scope such as 'self' or 'static', return that. if (self::isSelfTypeString($type_name) && $context->isInClassScope()) { return static::fromFullyQualifiedString((string) $context->getClassFQSEN()); } // Attach the context's namespace to the type name return self::fromNamespaceAndName($context->getNamespace() ?: '\\', $type_name); }