/**
  * @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()]);
 }
Example #2
0
 /**
  * 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);
         }
     }
 }
Example #3
0
 /**
  * Visit a node with kind `\ast\AST_METHOD_CALL`
  *
  * @param Node $node
  * A node of the type indicated by the method name that we'd
  * like to figure out the type that it produces.
  *
  * @return UnionType
  * The set of types that are possibly produced by the
  * given node
  */
 public function visitMethodCall(Node $node) : UnionType
 {
     $class_name = AST::classNameFromNode($this->context, $this->code_base, $node);
     if (empty($class_name)) {
         return new UnionType();
     }
     $class_fqsen = FullyQualifiedClassName::fromstringInContext($class_name, $this->context);
     assert($this->code_base->hasClassWithFQSEN($class_fqsen), "Class {$class_fqsen} must exist");
     $clazz = $this->code_base->getClassByFQSEN($class_fqsen);
     $method_name = $node->children['method'];
     // Give up on any complicated nonsense where the
     // method name is a variable such as in
     // `$variable->$function_name()`.
     if ($method_name instanceof Node) {
         return new UnionType();
     }
     // Method names can some times turn up being
     // other method calls.
     assert(is_string($method_name), "Method name must be a string. Something else given.");
     if (!$clazz->hasMethodWithName($this->code_base, $method_name)) {
         Log::err(Log::EUNDEF, "call to undeclared method {$class_fqsen}->{$method_name}()", $this->context->getFile(), $node->lineno);
         return new UnionType();
     }
     $method = $clazz->getMethodByNameInContext($this->code_base, $method_name, $this->context);
     return $method->getUnionType();
 }
Example #4
0
 /**
  * @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 '';
 }
 /**
  * @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()]);
 }
Example #6
0
 /**
  * @param Node $node
  * A node to parse
  *
  * @return Context
  * A new or an unchanged context resulting from
  * parsing the node
  */
 public function visitProp(Node $node) : Context
 {
     $property_name = $node->children['prop'];
     // Things like $foo->$bar
     if (!is_string($property_name)) {
         return $this->context;
     }
     assert(is_string($property_name), "Property must be string in context {$this->context}");
     try {
         $class_list = (new ContextNode($this->code_base, $this->context, $node->children['expr']))->getClassList();
     } catch (CodeBaseException $exception) {
         // This really shouldn't happen since the code
         // parsed cleanly. This should fatal.
         // throw $exception;
         return $this->context;
     } catch (\Exception $exception) {
         // If we can't figure out what kind of a class
         // this is, don't worry about it
         return $this->context;
     }
     foreach ($class_list as $clazz) {
         // Check to see if this class has the property or
         // a setter
         if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) {
             if (!$clazz->hasMethodWithName($this->code_base, '__set')) {
                 continue;
             }
         }
         try {
             $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context);
         } catch (IssueException $exception) {
             $exception->getIssueInstance()();
             return $this->context;
         }
         if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) {
             Issue::emit(Issue::TypeMismatchProperty, $this->context->getFile(), $node->lineno ?? 0, (string) $this->right_type, "{$clazz->getFQSEN()}::{$property->getName()}", (string) $property->getUnionType());
             return $this->context;
         }
         // After having checked it, add this type to it
         $property->getUnionType()->addUnionType($this->right_type);
         return $this->context;
     }
     if (Config::get()->allow_missing_properties) {
         try {
             // Create the property
             (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name);
         } catch (\Exception $exception) {
             // swallow it
         }
     } else {
         if (!empty($class_list)) {
             Issue::emit(Issue::UndeclaredProperty, $this->context->getFile(), $node->lineno ?? 0, $property_name);
         } else {
             // If we hit this part, we couldn't figure out
             // the class, so we ignore the issue
         }
     }
     return $this->context;
 }
Example #7
0
 /**
  * @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();
 }
Example #9
0
 /**
  * @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 '';
 }
Example #10
0
 /**
  * @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));
     }
 }
Example #11
0
 /**
  * @param Node $node
  * A node to parse
  *
  * @return Context
  * A new or an unchanged context resulting from
  * parsing the node
  */
 public function visitProp(Node $node) : Context
 {
     $property_name = $node->children['prop'];
     // Things like $foo->$bar
     if (!is_string($property_name)) {
         return $this->context;
     }
     assert(is_string($property_name), "Property must be string in context {$this->context}");
     try {
         $clazz = (new ContextNode($this->code_base, $this->context, $node))->getClass();
     } catch (CodeBaseException $exception) {
         Log::err(Log::EFATAL, $exception->getMessage(), $this->context->getFile(), $node->lineno);
     } catch (\Exception $exception) {
         // If we can't figure out what kind of a class
         // this is, don't worry about it
         return $this->context;
     }
     if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) {
         // Check to see if the class has a __set method
         if (!$clazz->hasMethodWithName($this->code_base, '__set')) {
             if (Config::get()->allow_missing_properties) {
                 try {
                     // Create the property
                     (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name);
                 } catch (\Exception $exception) {
                     // swallow it
                 }
             } else {
                 Log::err(Log::EAVAIL, "Missing property with name '{$property_name}'", $this->context->getFile(), $node->lineno);
             }
         }
         return $this->context;
     }
     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 $this->context;
     }
     if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) {
         Log::err(Log::ETYPE, "assigning {$this->right_type} to property but {$clazz->getFQSEN()}::{$property->getName()} is {$property->getUnionType()}", $this->context->getFile(), $node->lineno);
         return $this->context;
     }
     // After having checked it, add this type to it
     $property->getUnionType()->addUnionType($this->right_type);
     return $this->context;
 }
Example #12
0
 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();
 }
Example #13
0
 /**
  * @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 '';
 }
 public static function fromClosureInContext(Context $context) : FullyQualifiedFunctionName
 {
     $name = 'closure_' . substr(md5(implode('|', [$context->getFile(), $context->getLineNumberStart()])), 0, 12);
     return static::fromStringInContext($name, $context);
 }
Example #15
0
 /**
  * @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);
             }
         }
     }
 }
Example #16
0
 /**
  * @param CodeBase $code_base
  * The code base in which to find classes
  *
  * @param Context $context
  * The context in which we're resolving this union
  * type.
  *
  * @return Clazz[]
  * A list of classes representing the non-native types
  * associated with this UnionType
  *
  * @throws CodeBaseException
  * An exception is thrown if a non-native type does not have
  * an associated class
  */
 public function asClassList(CodeBase $code_base, Context $context)
 {
     // Iterate over each viable class type to see if any
     // have the constant we're looking for
     foreach ($this->nonNativeTypes()->getTypeSet() as $class_type) {
         // Get the class FQSEN
         $class_fqsen = $class_type->asFQSEN();
         if ($class_type->isStaticType()) {
             if (!$context->isInClassScope()) {
                 throw new IssueException(Issue::fromType(Issue::ContextNotObject)($context->getFile(), $context->getLineNumberStart(), [(string) $class_type]));
             }
             (yield $context->getClassInScope($code_base));
         } else {
             // See if the class exists
             if (!$code_base->hasClassWithFQSEN($class_fqsen)) {
                 throw new CodeBaseException($class_fqsen, "Cannot find class {$class_fqsen}");
             }
             (yield $code_base->getClassByFQSEN($class_fqsen));
         }
     }
 }
Example #17
0
File: Issue.php Project: etsy/phan
 /**
  * @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 (!Config::get()->disable_suppression && in_array($issue_type, Config::get()->suppress_issue_types ?? [])) {
         return;
     }
     // If a white-list of allowed issue types is defined,
     // only emit issues on the white-list
     if (!Config::get()->disable_suppression && count(Config::get()->whitelist_issue_types) > 0 && !in_array($issue_type, Config::get()->whitelist_issue_types ?? [])) {
         return;
     }
     if (!Config::get()->disable_suppression && $context->hasSuppressIssue($code_base, $issue_type)) {
         return;
     }
     Issue::emitWithParameters($issue_type, $context->getFile(), $lineno, $parameters);
 }
Example #18
0
 /**
  * @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 $issue_type)
 {
     if ($this->parent_node instanceof Node && $this->parent_node->kind == \ast\AST_STMT_LIST) {
         Issue::emit($issue_type, $this->context->getFile(), $node->lineno ?? 0);
     }
 }
Example #19
0
 /**
  * @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 IssueException
  * 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
 {
     // Check to see if we have the property
     if (!$code_base->hasProperty($this->getFQSEN(), $name)) {
         // If we don't have the property but do have a
         // __get method, then we can create the property
         if ($this->hasMethodWithName($code_base, '__get')) {
             $property = new Property($context, $name, new UnionType(), 0);
             $property->setFQSEN(FullyQualifiedPropertyName::make($this->getFQSEN(), $name));
             $this->addProperty($code_base, $property);
         } else {
             throw new IssueException(Issue::fromType(Issue::UndeclaredProperty)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$name}}"]));
         }
     }
     $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 IssueException(Issue::fromType(Issue::AccessPropertyPrivate)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$property->getName()}"]));
         }
         if ($property->isProtected()) {
             throw new IssueException(Issue::fromType(Issue::AccessPropertyProtected)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$property->getName()}"]));
         }
     }
     return $property;
 }
Example #20
0
 /**
  * @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);
 }
Example #21
0
 /**
  * @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());
             }
         }
     }
 }
Example #22
0
 /**
  * @return string
  * A unique and stable name for an anonymous class
  */
 public static function unqualifiedNameForAnonymousClassNode(Node $node, Context $context) : string
 {
     assert((bool) ($node->flags & \ast\flags\CLASS_ANONYMOUS), "Node must be an anonymous class node");
     $class_name = 'anonymous_class_' . substr(md5(implode('|', [$context->getFile(), $context->getLineNumberStart()])), 0, 8);
     return $class_name;
 }
Example #23
0
 /**
  * @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 ($context->hasSuppressIssue($code_base, $issue_type)) {
         return;
     }
     Issue::emitWithParameters($issue_type, $context->getFile(), $lineno, $parameters);
 }
Example #24
0
File: Clazz.php Project: etsy/phan
 /**
  * @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 IssueException
  * 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
 {
     // Get the FQSEN of the property we're looking for
     $property_fqsen = FullyQualifiedPropertyName::make($this->getFQSEN(), $name);
     $property = null;
     // Figure out if we have the property
     $has_property = $code_base->hasPropertyWithFQSEN($property_fqsen);
     // Figure out if the property is accessible
     $is_property_accessible = false;
     if ($has_property) {
         $property = $code_base->getPropertyByFQSEN($property_fqsen);
         $is_remote_access = !$context->isInClassScope() || !$context->getClassInScope($code_base)->getUnionType()->canCastToExpandedUnionType($this->getUnionType(), $code_base);
         $is_property_accessible = !$is_remote_access || $property->isPublic();
     }
     // If the property exists and is accessible, return it
     if ($is_property_accessible) {
         return $property;
     }
     // Check to see if we can use a __get magic method
     if ($this->hasMethodWithName($code_base, '__get')) {
         $method = $this->getMethodByName($code_base, '__get');
         // Make sure the magic method is accessible
         if ($method->isPrivate()) {
             throw new IssueException(Issue::fromType(Issue::AccessPropertyPrivate)($context->getFile(), $context->getLineNumberStart(), [(string) $property_fqsen]));
         } else {
             if ($method->isProtected()) {
                 throw new IssueException(Issue::fromType(Issue::AccessPropertyProtected)($context->getFile(), $context->getLineNumberStart(), [(string) $property_fqsen]));
             }
         }
         $property = new Property($context, $name, $method->getUnionType(), 0, $property_fqsen);
         $this->addProperty($code_base, $property, new None());
         return $property;
     } else {
         if ($has_property) {
             // If we have a property, but its inaccessible, emit
             // an issue
             if ($property->isPrivate()) {
                 throw new IssueException(Issue::fromType(Issue::AccessPropertyPrivate)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$property->getName()}"]));
             }
             if ($property->isProtected()) {
                 throw new IssueException(Issue::fromType(Issue::AccessPropertyProtected)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$property->getName()}"]));
             }
         }
     }
     // Check to see if missing properties are allowed
     // or we're stdclass
     if (Config::get()->allow_missing_properties || $this->getFQSEN() == FullyQualifiedClassName::getStdClassFQSEN()) {
         $property = new Property($context, $name, new UnionType(), 0, $property_fqsen);
         $this->addProperty($code_base, $property, new None());
         return $property;
     }
     throw new IssueException(Issue::fromType(Issue::UndeclaredProperty)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$name}}"]));
 }
Example #25
0
File: Plugin.php Project: etsy/phan
 /**
  * Emit an issue if it is not suppressed
  *
  * @param CodeBase $code_base
  * The code base in which the issue was found
  *
  * @param Context $context
  * The context in which the issue was found
  *
  * @param string $issue_type
  * A name for the type of issue such as 'PhanPluginMyIssue'
  *
  * @param string $issue_message
  * The complete issue message to emit such as 'class with
  * fqsen \NS\Name is broken in some fashion'.
  *
  * @param int $severity
  * A value from the set {Issue::SEVERITY_LOW,
  * Issue::SEVERITY_NORMAL, Issue::SEVERITY_HIGH}.
  *
  * @param int $remediation_difficulty
  * A guess at how hard the issue will be to fix from the
  * set {Issue:REMEDIATION_A, Issue:REMEDIATION_B, ...
  * Issue::REMEDIATION_F} with F being the hardest.
  */
 public function emitIssue(CodeBase $code_base, Context $context, string $issue_type, string $issue_message, int $severity = Issue::SEVERITY_NORMAL, int $remediation_difficulty = Issue::REMEDIATION_B, int $issue_type_id = Issue::TYPE_ID_UNKNOWN)
 {
     $issue = new Issue($issue_type, Issue::CATEGORY_PLUGIN, $severity, $issue_message, $remediation_difficulty, $issue_type_id);
     $issue_instance = new IssueInstance($issue, $context->getFile(), $context->getLineNumberStart(), []);
     Issue::maybeEmitInstance($code_base, $context, $issue_instance);
 }
Example #26
0
 /**
  * @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);
     }
 }
Example #27
0
 /**
  * @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 '';
 }