public function infer(array $components) { $this->components = $components; $resolved = new \SplObjectStorage(); $unresolved = new \SplObjectStorage(); foreach ($components['variables'] as $op) { if (!empty($op->type) && $op->type->type !== Type::TYPE_UNKNOWN && $op->type->type !== Type::TYPE_MIXED) { $resolved[$op] = $op->type; } elseif ($op instanceof Operand\Literal) { $resolved[$op] = Type::fromValue($op->value); } else { $unresolved[$op] = Type::getAllPosibilities(); } } if (count($unresolved) === 0) { // short-circuit return; } do { echo "Round " . $round++ . " (" . count($unresolved) . " unresolved variables out of " . count($components['variables']) . ")\n"; $start = round(count($resolved) / count($unresolved), 6); $i = 0; $toRemove = []; foreach ($unresolved as $k => $var) { $i++; if ($i % 10 === 0) { echo "."; } if ($i % 800 === 0) { echo "\n"; } $type = $this->resolveVar($var, $resolved); if ($type) { $toRemove[] = $var; $resolved[$var] = $type; } } foreach ($toRemove as $remove) { $unresolved->detach($remove); } echo "\n"; } while (count($unresolved) > 0 && $start < round(count($resolved) / count($unresolved), 6)); foreach ($resolved as $var) { $var->type = $resolved[$var]; } foreach ($unresolved as $var) { $var->type = new Type(Type::TYPE_UNKNOWN); } }
protected function verifyReturn($function, array $components) { if (!$function->stmts) { // interface return []; } $errors = []; if ($function->returnType) { $type = Type::fromDecl($function->returnType->value); } else { $type = Type::extractTypeFromComment("return", $function->getAttribute('doccomment')); if (Type::mixed()->equals($type)) { // only verify actual types return $errors; } } $returns = $this->findReturnBlocks($function->stmts); foreach ($returns as $return) { if (!$return || !$return->expr) { // Default return, no if ($type->allowsNull()) { continue; } if (!$return) { $errors[] = ["Default return found for non-null type {$type}", $function]; } else { $errors[] = ["Explicit null return found for non-null type {$type}", $return]; } } elseif (!$return->expr->type) { var_dump($return->expr); $errors[] = ["Could not resolve type for return", $return]; } else { if (!$components['typeResolver']->resolves($return->expr->type, $type)) { $errors[] = ["Type mismatch on return value, found {$return->expr->type} expecting {$type}", $return]; } } } return $errors; }
protected function processTypeAssertion(Assertion\TypeAssertion $assertion, Operand $source, \SplObjectStorage $resolved) { if ($assertion->value instanceof Operand) { if ($assertion->value instanceof Operand\Literal) { return Type::fromDecl($assertion->value->value); } if (isset($resolved[$assertion->value])) { return $resolved[$assertion->value]; } return false; } $subTypes = []; foreach ($assertion->value as $sub) { $subTypes[] = $subType = $this->processTypeAssertion($sub, $source, $resolved); if (!$subType) { // Not fully resolved yet return false; } } $type = $assertion->mode === Assertion::MODE_UNION ? Type::TYPE_UNION : Type::TYPE_INTERSECTION; return new Type($type, $subTypes); }
protected function verifyInternalCall($func, $call, $components, $name) { $errors = []; foreach ($func['params'] as $idx => $param) { if (!isset($call->args[$idx])) { if (substr($param['name'], -1) !== '=') { $errors[] = ["Missing required argument {$idx} for call {$name}()", $call]; } continue; } if ($param['type']) { $type = Type::fromDecl($param['type']); if (is_string($call->args[$idx]->type)) { $call->args[$idx]->type = Type::fromDecl($call->args[$idx]->type); } if (!$components['typeResolver']->resolves($call->args[$idx]->type, $type)) { $errors[] = ["Type mismatch on {$name}() argument {$idx}, found {$call->args[$idx]->type} expecting {$type}", $call]; } } } return $errors; }
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]; } $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 [new Type(Type::TYPE_MIXED)]; } break; case 'Expr_Assign': case 'Expr_AssignRef': if ($resolved->contains($op->expr)) { return [$resolved[$op->expr]]; } break; 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_InstanceOf': case 'Expr_Isset': return [new Type(Type::TYPE_BOOLEAN)]; 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 [new Type(Type::TYPE_STRING)]; default: return [new Type(Type::TYPE_LONG)]; } } break; case 'Expr_BitwiseNot': if ($resolved->contains($op->expr)) { if ($resolved[$op->expr]->type === Type::TYPE_STRING) { return [new Type(Type::TYPE_STRING)]; } return [new Type(Type::TYPE_LONG)]; } 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 [new Type(Type::TYPE_LONG)]; case [Type::TYPE_DOUBLE, TYPE::TYPE_LONG]: case [Type::TYPE_LONG, TYPE::TYPE_DOUBLE]: case [Type::TYPE_DOUBLE, TYPE::TYPE_DOUBLE]: case [Type::TYPE_MIXED, TYPE::TYPE_DOUBLE]: case [Type::TYPE_DOUBLE, TYPE::TYPE_MIXED]: case [Type::TYPE_NUMERIC, TYPE::TYPE_DOUBLE]: case [Type::TYPE_DOUBLE, TYPE::TYPE_NUMERIC]: return [new Type(Type::TYPE_DOUBLE)]; case [Type::TYPE_MIXED, Type::TYPE_MIXED]: case [Type::TYPE_MIXED, Type::TYPE_LONG]: case [Type::TYPE_LONG, Type::TYPE_MIXED]: case [Type::TYPE_NUMERIC, Type::TYPE_LONG]: case [Type::TYPE_LONG, Type::TYPE_NUMERIC]: case [Type::TYPE_NUMERIC, Type::TYPE_MIXED]: case [Type::TYPE_MIXED, Type::TYPE_NUMERIC]: case [Type::TYPE_NUMERIC, Type::TYPE_NUMERIC]: return [new Type(Type::TYPE_NUMERIC)]; 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: return [new Type(Type::TYPE_MIXED)]; } } break; case 'Expr_BinaryOp_Concat': case 'Expr_Cast_String': case 'Expr_ConcatList': return [new Type(Type::TYPE_STRING)]; case 'Expr_BinaryOp_Mod': case 'Expr_BinaryOp_ShiftLeft': case 'Expr_BinaryOp_ShiftRight': case 'Expr_Cast_Int': case 'Expr_Print': return [new Type(Type::TYPE_LONG)]; case 'Expr_Cast_Double': return [new Type(Type::TYPE_DOUBLE)]; case 'Expr_Cast_Object': if ($resolved->contains($op->expr)) { if ($resolved[$op->expr]->type === Type::TYPE_USER) { return [$resolved[$op->expr]]; } return [new Type(Type::TYPE_USER, null, 'stdClass')]; } break; case 'Expr_Clone': if ($resolved->contains($op->expr)) { return [$resolved[$op->expr]]; } break; case 'Expr_Closure': return [new Type(Type::TYPE_USER, [], ["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 [new Type(Type::TYPE_MIXED)]; } return [Type::fromDecl($type['return'])]; } } } // we can't resolve the function return [new Type(Type::TYPE_MIXED)]; break; case 'Expr_List': if ($op->result === $var) { return [new Type(Type::TYPE_ARRAY)]; } // TODO: infer this return [new Type(Type::TYPE_MIXED)]; case 'Expr_New': if ($op->class instanceof Operand\Literal) { return [new Type(Type::TYPE_USER, [], [$op->class->value])]; } return [new Type(Type::TYPE_OBJECT)]; case 'Expr_Param': 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->type |= Type::TYPE_NULL; } } return [$type]; } return [Type::extractTypeFromComment("param", $op->function->getAttribute('doccomment'), $op->name->value)]; case 'Expr_Yield': case 'Expr_Include': case 'Expr_PropertyFetch': case 'Expr_StaticPropertyFetch': case 'Stmt_Property': // TODO: we may be able to determine these... return [new Type(Type::TYPE_MIXED)]; case 'Expr_TypeAssert': return [Type::fromDecl($op->assertedType)]; case 'Expr_TypeUnAssert': if ($resolved->contains($op->assert->expr)) { return [$resolved[$op->assert->expr]]; } 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 [new Type(Type::TYPE_NUMERIC)]; } break; case 'Expr_Eval': return [new Type(Type::TYPE_MIXED)]; case 'Iterator_Key': if ($resolved->contains($op->var)) { // TODO: implement this as well return [new Type(Type::TYPE_MIXED)]; } break; case 'Expr_Exit': case 'Iterator_Reset': return [new Type(Type::TYPE_VOID)]; case 'Iterator_Valid': return [new Type(Type::TYPE_BOOLEAN)]; case 'Iterator_Value': if ($resolved->contains($op->var)) { if ($resolved[$op->var]->subTypes) { return $resolved[$op->var]->subTypes; } return [new Type(Type::TYPE_MIXED)]; } break; case 'Expr_MethodCall': if (!$op->name instanceof Operand\Literal) { // variable method call return [new Type(Type::TYPE_MIXED)]; } if ($resolved->contains($op->var)) { if ($resolved[$op->var]->type !== Type::TYPE_USER) { return [new Type(Type::TYPE_MIXED)]; } $types = []; foreach ($resolved[$op->var]->userTypes as $ut) { $className = strtolower($ut); if (!isset($this->components['resolves'][$className])) { return [new Type(Type::TYPE_MIXED)]; } foreach ($this->components['resolves'][$className] as $class) { $method = $this->findMethod($class, $op->name->value); if (!$method) { continue; } if (!$method->returnType) { $types[] = Type::extractTypeFromComment("return", $method->getAttribute('doccomment')); } else { $types[] = Type::fromDecl($method->returnType->value); } } } return $types; } break; case 'Expr_ConstFetch': if ($op->name instanceof Operand\Literal) { $constant = strtolower($op->name->value); switch ($constant) { case 'true': case 'false': return [new Type(Type::TYPE_BOOLEAN)]; case 'null': return [new Type(Type::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 [new Type(Type::TYPE_MIXED)]; case 'Expr_StaticCall': return [new Type(Type::TYPE_MIXED)]; 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_USER) { // give up return [new Type(Type::TYPE_MIXED)]; } $types = []; foreach ($type->userTypes as $type) { $try = $this->resolveClassConstant(strtolower($type), $op, $resolved); if ($try) { $types = array_merge($types, $try); } else { return false; } } if ($types) { return $types; } return [new Type(Type::TYPE_MIXED)]; } return false; case 'Phi': $types = []; foreach ($op->vars as $var) { if ($resolved->contains($var)) { $types[] = $resolved[$var]; } else { // entire phi isn't resolved yet, can't process continue 2; } } if (empty($types)) { return false; } $type = $this->computeMergedType($types); if ($type) { return [$type]; } return false; default: throw new \RuntimeException("Unknown operand prefix type: " . $op->getType()); } return false; }