protected function resolveVarOp(Operand $var, Op $op, \SplObjectStorage $resolved) { switch ($op->getType()) { case 'Expr_Array': $types = []; foreach ($op->values as $value) { if (!isset($resolved[$value])) { return false; } $types[] = $resolved[$value]; } if (empty($types)) { return [new Type(Type::TYPE_ARRAY)]; } $r = $this->computeMergedType($types); if ($r) { return [new Type(Type::TYPE_ARRAY, [$r])]; } case 'Expr_Cast_Array': // Todo: determine subtypes better return [new Type(Type::TYPE_ARRAY)]; case 'Expr_ArrayDimFetch': if ($resolved->contains($op->var)) { // Todo: determine subtypes better $type = $resolved[$op->var]; if ($type->subTypes) { return $type->subTypes; } if ($type->type === Type::TYPE_STRING) { return [$type]; } return [Type::mixed()]; } break; case 'Expr_Assign': case 'Expr_AssignRef': if ($resolved->contains($op->expr)) { return [$resolved[$op->expr]]; } break; case 'Expr_InstanceOf': case 'Expr_BinaryOp_Equal': case 'Expr_BinaryOp_NotEqual': case 'Expr_BinaryOp_Greater': case 'Expr_BinaryOp_GreaterOrEqual': case 'Expr_BinaryOp_Identical': case 'Expr_BinaryOp_NotIdentical': case 'Expr_BinaryOp_Smaller': case 'Expr_BinaryOp_SmallerOrEqual': case 'Expr_BinaryOp_LogicalAnd': case 'Expr_BinaryOp_LogicalOr': case 'Expr_BinaryOp_LogicalXor': case 'Expr_BooleanNot': case 'Expr_Cast_Bool': case 'Expr_Empty': case 'Expr_Isset': return [Type::bool()]; case 'Expr_BinaryOp_BitwiseAnd': case 'Expr_BinaryOp_BitwiseOr': case 'Expr_BinaryOp_BitwiseXor': if ($resolved->contains($op->left) && $resolved->contains($op->right)) { switch ([$resolved[$op->left]->type, $resolved[$op->right]->type]) { case [Type::TYPE_STRING, Type::TYPE_STRING]: return [Type::string()]; default: return [Type::int()]; } } break; case 'Expr_BitwiseNot': if ($resolved->contains($op->expr)) { if ($resolved[$op->expr]->type === Type::TYPE_STRING) { return [Type::string()]; } return [Type::int()]; } break; case 'Expr_BinaryOp_Div': case 'Expr_BinaryOp_Plus': case 'Expr_BinaryOp_Minus': case 'Expr_BinaryOp_Mul': if ($resolved->contains($op->left) && $resolved->contains($op->right)) { switch ([$resolved[$op->left]->type, $resolved[$op->right]->type]) { case [Type::TYPE_LONG, Type::TYPE_LONG]: return [Type::int()]; case [Type::TYPE_DOUBLE, TYPE::TYPE_LONG]: case [Type::TYPE_LONG, TYPE::TYPE_DOUBLE]: case [Type::TYPE_DOUBLE, TYPE::TYPE_DOUBLE]: return [Type::float()]; case [Type::TYPE_ARRAY, Type::TYPE_ARRAY]: $sub = $this->computeMergedType(array_merge($resolved[$op->left]->subTypes, $resolved[$op->right]->subTypes)); if ($sub) { return [new Type(Type::TYPE_ARRAY, [$sub])]; } return [new Type(Type::TYPE_ARRAY)]; default: throw new \RuntimeException("Math op on unknown types {$resolved[$op->left]} + {$resolved[$op->right]}"); } } break; case 'Expr_BinaryOp_Concat': case 'Expr_Cast_String': case 'Expr_ConcatList': return [Type::string()]; case 'Expr_BinaryOp_Mod': case 'Expr_BinaryOp_ShiftLeft': case 'Expr_BinaryOp_ShiftRight': case 'Expr_Cast_Int': case 'Expr_Print': return [Type::int()]; case 'Expr_Cast_Double': return [Type::float()]; case 'Expr_Cast_Object': if ($resolved->contains($op->expr)) { if ($resolved[$op->expr]->type->resolves(Type::object())) { return [$resolved[$op->expr]]; } return [new Type(Type::TYPE_OBJECT, [], 'stdClass')]; } break; case 'Expr_Clone': if ($resolved->contains($op->expr)) { return [$resolved[$op->expr]]; } break; case 'Expr_Closure': return [new Type(Type::TYPE_OBJECT, [], "Closure")]; case 'Expr_FuncCall': if ($op->name instanceof Operand\Literal) { $name = strtolower($op->name->value); if (isset($this->components['functionLookup'][$name])) { $result = []; foreach ($this->components['functionLookup'][$name] as $func) { if ($func->returnType) { $result[] = Type::fromDecl($func->returnType->value); } else { // Check doc comment $result[] = Type::extractTypeFromComment("return", $func->getAttribute('doccomment')); } } return $result; } else { if (isset($this->components['internalTypeInfo']->functions[$name])) { $type = $this->components['internalTypeInfo']->functions[$name]; if (empty($type['return'])) { return false; } return [Type::fromDecl($type['return'])]; } } } // we can't resolve the function return false; case 'Expr_List': if ($op->result === $var) { return [new Type(Type::TYPE_ARRAY)]; } // TODO: infer this return false; case 'Expr_New': $type = $this->getClassType($op->class, $resolved); if ($type) { return [$type]; } return [Type::object()]; case 'Expr_Param': $docType = Type::extractTypeFromComment("param", $op->function->getAttribute('doccomment'), $op->name->value); if ($op->type) { $type = Type::fromDecl($op->type->value); if ($op->defaultVar) { if ($op->defaultBlock->children[0]->getType() === "Expr_ConstFetch" && strtolower($op->defaultBlock->children[0]->name->value) === "null") { $type = (new Type(Type::TYPE_UNION, [$type, Type::null()]))->simplify(); } } if ($docType !== Type::mixed() && $this->components['typeResolver']->resolves($docType, $type)) { // return the more specific return [$docType]; } return [$type]; } return [$docType]; case 'Expr_PropertyFetch': case 'Expr_StaticPropertyFetch': if (!$op->name instanceof Operand\Literal) { // variable property fetch return [Type::mixed()]; } $propName = $op->name->value; if ($op instanceof Op\Expr\StaticPropertyFetch) { $objType = $this->getClassType($op->class, $resolved); } else { $objType = $this->getClassType($op->var, $resolved); } if ($objType) { return $this->resolveProperty($objType, $propName); } return false; case 'Expr_Yield': case 'Expr_Include': // TODO: we may be able to determine these... return false; case 'Expr_Assertion': $tmp = $this->processAssertion($op->assertion, $op->expr, $resolved); if ($tmp) { return [$tmp]; } return false; case 'Expr_TypeUnAssert': throw new \RuntimeException("Unassertions should not occur anymore"); case 'Expr_UnaryMinus': case 'Expr_UnaryPlus': if ($resolved->contains($op->expr)) { switch ($resolved[$op->expr]->type) { case Type::TYPE_LONG: case Type::TYPE_DOUBLE: return [$resolved[$op->expr]]; } return [Type::numeric()]; } break; case 'Expr_Eval': return false; case 'Iterator_Key': if ($resolved->contains($op->var)) { // TODO: implement this as well return false; } break; case 'Expr_Exit': case 'Iterator_Reset': return [Type::null()]; case 'Iterator_Valid': return [Type::bool()]; case 'Iterator_Value': if ($resolved->contains($op->var)) { if ($resolved[$op->var]->subTypes) { return $resolved[$op->var]->subTypes; } return false; } break; case 'Expr_StaticCall': return $this->resolveMethodCall($op->class, $op->name, $op, $resolved); case 'Expr_MethodCall': return $this->resolveMethodCall($op->var, $op->name, $op, $resolved); case 'Expr_ConstFetch': if ($op->name instanceof Operand\Literal) { $constant = strtolower($op->name->value); switch ($constant) { case 'true': case 'false': return [Type::bool()]; case 'null': return [Type::null()]; default: if (isset($this->components['constants'][$op->name->value])) { $return = []; foreach ($this->components['constants'][$op->name->value] as $value) { if (!$resolved->contains($value->value)) { return false; } $return[] = $resolved[$value->value]; } return $return; } } } return false; case 'Expr_ClassConstFetch': //TODO $classes = []; if ($op->class instanceof Operand\Literal) { $class = strtolower($op->class->value); return $this->resolveClassConstant($class, $op, $resolved); } elseif ($resolved->contains($op->class)) { $type = $resolved[$op->class]; if ($type->type !== Type::TYPE_OBJECT || empty($type->userType)) { // give up return false; } return $this->resolveClassConstant(strtolower($type->userType), $op, $resolved); } return false; case 'Phi': $types = []; $resolveFully = true; foreach ($op->vars as $v) { if ($resolved->contains($v)) { $types[] = $resolved[$v]; } else { $resolveFully = false; } } if (empty($types)) { return false; } $type = $this->computeMergedType($types); if ($type) { if ($resolveFully) { return [$type]; } // leave on unresolved list to try again next round $resolved[$var] = $type; } return false; default: throw new \RuntimeException("Unknown operand prefix type: " . $op->getType()); } return false; }