Support checking option type typehints at runtime.

Adds support for checking ?Foo type hints in VerifyParamType.
The parameter must have type Foo or null.  Failing to pass the hint is
reported as a warning instead of a recoverable error for now for
migration reasons---we'll want to convert it to be the same as normal
type hints later.
Esse commit está contido em:
Jordan DeLong
2013-06-06 12:29:02 -07:00
commit de Sara Golemon
commit 6b891c8331
27 arquivos alterados com 295 adições e 95 exclusões
+3
Ver Arquivo
@@ -3345,6 +3345,9 @@ public:
std::map<std::string,int>::iterator it =
m_gidMap.find("v:" + p->getName());
if (it != m_gidMap.end() && it->second) {
// NB: this is unsound if the user error handler swallows a
// parameter typehint failure. It's opt-in via compiler
// options, though.
if (useDefaults && p->hasTypeHint() && !p->defaultValue()) {
b->setBit(DataFlow::AvailIn, it->second);
}
+49 -21
Ver Arquivo
@@ -206,11 +206,11 @@ class FuncFinisher {
public:
FuncFinisher(EmitterVisitor* ev, Emitter& e, FuncEmitter* fe)
: m_ev(ev), m_e(e), m_fe(fe) {
TRACE(1, "FuncFinisher constructed: %s %p\n", m_fe->name()->data(), m_fe);
TRACE(2, "FuncFinisher constructed: %s\n", m_fe->name()->data());
}
~FuncFinisher() {
TRACE(1, "Finishing func: %s %p\n", m_fe->name()->data(), m_fe);
TRACE(2, "Finishing func: %s\n", m_fe->name()->data());
m_ev->finishFunc(m_e, m_fe);
}
};
@@ -5265,6 +5265,41 @@ static Attr buildAttrs(ModifierExpressionPtr mod, bool isRef = false) {
return Attr(attrs);
}
static TypeConstraint
determine_type_constraint(const ParameterExpressionPtr& par) {
if (par->hasTypeHint()) {
auto ce = dynamic_pointer_cast<ConstantExpression>(par->defaultValue());
auto flags = TypeConstraint::NoFlags;
if (ce && ce->isNull()) {
flags = flags|TypeConstraint::Nullable;
}
if (par->hhType()) {
flags = flags|TypeConstraint::HHType;
}
return TypeConstraint{
StringData::GetStaticString(par->getOriginalTypeHint()),
flags
};
}
if (auto annot = par->annotation()) {
if (!annot->isNullable()) return {};
if (annot->isSoft() || annot->isFunction()) return {};
auto strippedName = annot->stripNullable().vanillaName();
if (strippedName.empty()) return {};
return TypeConstraint{
StringData::GetStaticString(strippedName),
TypeConstraint::Nullable |
TypeConstraint::ExtendedHint |
TypeConstraint::HHType
};
}
return {};
}
void EmitterVisitor::emitPostponedMeths() {
vector<FuncEmitter*> top_fes;
while (!m_postponedMeths.empty()) {
@@ -5321,23 +5356,17 @@ void EmitterVisitor::emitPostponedMeths() {
if (par->isOptional()) {
dvInitializers.push_back(DVInitializer(i, par->defaultValue()));
}
// Will be fixed up later, when the DV initializers are emitted.
FuncEmitter::ParamInfo pi;
if (par->hasTypeHint()) {
ConstantExpressionPtr ce =
dynamic_pointer_cast<ConstantExpression>(par->defaultValue());
bool nullable = ce && ce->isNull();
TypeConstraint tc =
TypeConstraint(
StringData::GetStaticString(par->getOriginalTypeHint()),
nullable,
par->hhType());
pi.setTypeConstraint(tc);
TRACE(1, "Added constraint to %s\n", fe->name()->data());
auto const typeConstraint = determine_type_constraint(par);
if (typeConstraint.hasConstraint()) {
pi.setTypeConstraint(typeConstraint);
}
if (par->hasUserType()) {
pi.setUserType(StringData::GetStaticString(par->getUserTypeHint()));
}
// Store info about the default value if there is one.
if (par->isOptional()) {
const StringData* phpCode;
@@ -5499,10 +5528,7 @@ void EmitterVisitor::emitPostponedMeths() {
}
for (uint i = 0; i < fe->params().size(); i++) {
const TypeConstraint& tc = fe->params()[i].typeConstraint();
if (!tc.exists()) continue;
TRACE(2, "permanent home for tc %s, param %d of func %s: %p\n",
tc.typeName()->data(), i, fe->name()->data(), &tc);
assert(tc.typeName()->data() != (const char*)0xdeadba5eba11f00d);
if (!tc.hasConstraint()) continue;
e.VerifyParamType(i);
}
@@ -7333,9 +7359,11 @@ void emitAllHHBC(AnalysisResultPtr ar) {
/* there is a race condition in the first call to
GetStaticString. Make sure we dont hit it */
StringData::GetStaticString("");
/* same for TypeConstraint */
TypeConstraint tc;
{
StringData::GetStaticString("");
/* same for TypeConstraint */
TypeConstraint tc;
}
JobQueueDispatcher<EmitterWorker::JobType, EmitterWorker>
dispatcher(threadCount, true, 0, false, ar.get());
+22 -13
Ver Arquivo
@@ -13,9 +13,9 @@
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/compiler/expression/parameter_expression.h"
#include "hphp/compiler/type_annotation.h"
#include "hphp/compiler/expression/parameter_expression.h"
#include "hphp/compiler/analysis/function_scope.h"
#include "hphp/compiler/analysis/file_scope.h"
#include "hphp/compiler/analysis/variable_table.h"
@@ -30,14 +30,23 @@ using namespace HPHP;
///////////////////////////////////////////////////////////////////////////////
// constructors/destructors
ParameterExpression::ParameterExpression
(EXPRESSION_CONSTRUCTOR_PARAMETERS,
TypeAnnotationPtr type, bool hhType, const std::string &name, bool ref,
ExpressionPtr defaultValue, ExpressionPtr attributeList)
: Expression(EXPRESSION_CONSTRUCTOR_PARAMETER_VALUES(ParameterExpression)),
m_originalType(type), m_name(name), m_hhType(hhType), m_ref(ref),
m_defaultValue(defaultValue), m_attributeList(attributeList) {
m_type = Util::toLower(type ? type->simpleName() : "");
ParameterExpression::ParameterExpression(
EXPRESSION_CONSTRUCTOR_PARAMETERS,
TypeAnnotationPtr type,
bool hhType,
const std::string &name,
bool ref,
ExpressionPtr defaultValue,
ExpressionPtr attributeList)
: Expression(EXPRESSION_CONSTRUCTOR_PARAMETER_VALUES(ParameterExpression))
, m_originalType(type)
, m_name(name)
, m_hhType(hhType)
, m_ref(ref)
, m_defaultValue(defaultValue)
, m_attributeList(attributeList)
{
m_type = Util::toLower(type ? type->vanillaName() : "");
if (m_defaultValue) {
m_defaultValue->setContext(InParameterExpression);
}
@@ -51,12 +60,12 @@ ExpressionPtr ParameterExpression::clone() {
return exp;
}
const std::string ParameterExpression::getOriginalTypeHint() const {
const std::string ParameterExpression::getOriginalTypeHint() const {
assert(hasTypeHint());
return m_originalType->simpleName();
return m_originalType->vanillaName();
}
const std::string ParameterExpression::getUserTypeHint() const {
const std::string ParameterExpression::getUserTypeHint() const {
assert(hasUserType());
return m_originalType->fullName();
}
@@ -286,7 +295,7 @@ void ParameterExpression::compatibleDefault() {
// code generation functions
void ParameterExpression::outputPHP(CodeGenerator &cg, AnalysisResultPtr ar) {
if (!m_type.empty()) cg_printf("%s ", m_originalType->simpleName().c_str());
if (!m_type.empty()) cg_printf("%s ", m_originalType->vanillaName().c_str());
if (m_ref) cg_printf("&");
cg_printf("$%s", m_name.c_str());
if (m_defaultValue) {
@@ -18,6 +18,7 @@
#define incl_HPHP_PARAMETER_EXPRESSION_H_
#include "hphp/compiler/expression/expression.h"
#include "hphp/compiler/expression/constant_expression.h"
#include "hphp/util/json.h"
namespace HPHP {
@@ -46,11 +47,15 @@ public:
ExpressionPtr defaultValue() { return m_defaultValue; }
ExpressionPtr userAttributeList() { return m_attributeList; }
TypePtr getTypeSpec(AnalysisResultPtr ar, bool forInference);
bool hasTypeHint() const { return !m_type.empty(); }
const std::string &getTypeHint() const {
assert(hasTypeHint());
return m_type;
}
TypeAnnotationPtr annotation() const { return m_originalType; }
bool hasUserType() const { return m_originalType != nullptr; }
const std::string getOriginalTypeHint() const;
const std::string getUserTypeHint() const;
+1
Ver Arquivo
@@ -916,6 +916,7 @@ void Parser::onParam(Token &out, Token *params, Token &type, Token &var,
if (attr && attr->exp) {
attrList = dynamic_pointer_cast<ExpressionList>(attr->exp);
}
TypeAnnotationPtr typeAnnotation = type.typeAnnotation;
expList->addElement(NEW_EXP(ParameterExpression, typeAnnotation,
m_scanner.hipHopSyntaxEnabled(), var->text(),
+2 -2
Ver Arquivo
@@ -33,7 +33,7 @@ TypeAnnotation::TypeAnnotation(const std::string &name,
m_xhp(false),
m_typevar(false) {}
const std::string TypeAnnotation::simpleName() const {
std::string TypeAnnotation::vanillaName() const {
// filter out types that should not be exposed to the runtime
if (m_nullable || m_soft || m_typevar || m_function) {
return "";
@@ -44,7 +44,7 @@ const std::string TypeAnnotation::simpleName() const {
return m_name;
}
const std::string TypeAnnotation::fullName() const {
std::string TypeAnnotation::fullName() const {
std::string name;
if (m_nullable) {
name += '?';
+43 -2
Ver Arquivo
@@ -73,9 +73,50 @@ public:
void setXHP() { m_xhp = true; }
void setTypeVar() { m_typevar = true; }
const std::string simpleName() const;
const std::string fullName() const;
bool isNullable() const { return m_nullable; }
bool isSoft() const { return m_soft; }
bool isTuple() const { return m_tuple; }
bool isFunction() const { return m_function; }
bool isXHP() const { return m_xhp; }
bool isTypeVar() const { return m_typevar; }
/*
* Return a shallow copy of this TypeAnnotation, except with
* nullability stripped.
*/
TypeAnnotation stripNullable() const {
auto ret = *this;
ret.m_nullable = false;
return ret;
}
/*
* Returns whether this TypeAnnotation is "simple"---as described
* above, this implies it has only one level of depth. Both the
* type list and type args are null.
*
* It may however be soft or nullable, or a function type, etc.
*/
bool isSimple() const { return !m_typeList && !m_typeArgs; }
/*
* Return a string for this annotation that is a type hint for
* normal "vanilla" php. This means <?hh-specific annotations (such
* as ?Foo or @Foo) are going to be stripped, as well as the deep
* information about a type. (E.g. for Vector<string> this will
* return "Vector".)
*/
std::string vanillaName() const;
/*
* Returns a complete string name of this type-annotation, including
* <?hh-specific extensions, any type parameter list, etc.
*/
std::string fullName() const;
/*
* Add a new element to this type list for this TypeAnnotation.
*/
void appendToTypeList(TypeAnnotationPtr typeList);
private:
-23
Ver Arquivo
@@ -681,29 +681,6 @@ void throw_call_non_object(const char *methodName) {
throw FatalErrorException(msg.c_str());
}
void throw_unexpected_argument_type(int argNum, const char *fnName,
const char *expected, CVarRef val) {
const char *otype = nullptr;
switch (val.getType()) {
case KindOfUninit:
case KindOfNull: otype = "null"; break;
case KindOfBoolean: otype = "bool"; break;
case KindOfInt64: otype = "int"; break;
case KindOfDouble: otype = "double"; break;
case KindOfStaticString:
case KindOfString: otype = "string"; break;
case KindOfArray: otype = "array"; break;
case KindOfObject:
otype = val.getObjectData()->o_getClassName().c_str();
break;
default:
assert(false);
}
raise_recoverable_error
("Argument %d passed to %s must be an instance of %s, %s given",
argNum, fnName, expected, otype);
}
Object f_clone(CVarRef v) {
if (v.isObject()) {
Object clone = Object(v.toObject()->clone());
-2
Ver Arquivo
@@ -333,8 +333,6 @@ void throw_toomany_arguments_nr(const char *fn, int num, int level = 0)
__attribute__((cold));
void throw_wrong_arguments_nr(const char *fn, int count, int cmin, int cmax,
int level = 0) __attribute__((cold));
void throw_unexpected_argument_type(int argNum, const char *fnName,
const char *expected, CVarRef val);
/**
* Handler for exceptions thrown from user functions that we don't
+1
Ver Arquivo
@@ -394,6 +394,7 @@ public:
kEvalVMInitialGlobalTableSizeDefault) \
F(bool, Jit, evalJitDefault()) \
F(bool, AllowHhas, false) \
F(bool, CheckExtendedTypeHints, true) \
F(bool, JitNoGdb, true) \
F(bool, PerfPidMap, true) \
F(bool, KeepPerfPidMap, false) \
+2 -2
Ver Arquivo
@@ -423,7 +423,7 @@ static void set_function_info(Array &ret, const Func* func) {
param.set(s_index, VarNR((int)i));
VarNR name(func->localNames()[i]);
param.set(s_name, name);
const StringData* type = fpi.typeConstraint().exists() ?
const StringData* type = fpi.typeConstraint().hasConstraint() ?
fpi.typeConstraint().typeName() : empty_string.get();
param.set(s_type, VarNR(type));
const StringData* typeHint = fpi.userType() ?
@@ -434,7 +434,7 @@ static void set_function_info(Array &ret, const Func* func) {
param.set(s_class, VarNR(func->cls() ? func->cls()->name() :
func->preClass()->name()));
}
if (!fpi.typeConstraint().exists() ||
if (!fpi.typeConstraint().hasConstraint() ||
fpi.typeConstraint().nullable()) {
param.set(s_nullable, true_varNR);
}
+1 -1
Ver Arquivo
@@ -6608,7 +6608,7 @@ inline void OPTBLD_INLINE VMExecutionContext::iopVerifyParamType(PC& pc) {
assert(param < func->numParams());
assert(func->numParams() == int(func->params().size()));
const TypeConstraint& tc = func->params()[param].typeConstraint();
assert(tc.exists());
assert(tc.hasConstraint() || !RuntimeOption::EvalCheckExtendedTypeHints);
const TypedValue *tv = frame_local(m_fp, param);
tc.verify(tv, func, param);
}
+1 -1
Ver Arquivo
@@ -598,7 +598,7 @@ void Func::getFuncInfo(ClassInfo::MethodInfo* mi) const {
// owned by ParamInfo.
pi->valueText = fpi.phpCode()->data();
}
pi->type = fpi.typeConstraint().exists() ?
pi->type = fpi.typeConstraint().hasConstraint() ?
fpi.typeConstraint().typeName()->data() : "";
for (UserAttributeMap::const_iterator it = fpi.userAttributes().begin();
it != fpi.userAttributes().end(); ++it) {
+63 -10
Ver Arquivo
@@ -30,11 +30,21 @@ namespace HPHP {
TRACE_SET_MOD(runtime);
namespace {
// TODO(#2322864): this is a hack until we can get rid of the "Xhp"
// psuedo-type.
const StaticString s_xhp("Xhp");
bool blacklistedName(const StringData* sd) {
if (!sd) return false;
return sd->isame(s_xhp.get());
}
}
TypeConstraint::TypeMap TypeConstraint::s_typeNamesToTypes;
void TypeConstraint::init() {
const StringData* typeName = m_typeName;
if (UNLIKELY(s_typeNamesToTypes.empty())) {
const struct Pair {
const StringData* name;
@@ -75,7 +85,16 @@ void TypeConstraint::init() {
}
}
if (typeName == nullptr) {
if (m_typeName && isExtended()) {
assert(nullable() &&
"Only nullable extended type hints are implemented");
}
if (blacklistedName(m_typeName) ||
(isExtended() && !RuntimeOption::EvalCheckExtendedTypeHints)) {
m_typeName = nullptr;
}
if (m_typeName == nullptr) {
m_type.m_dt = KindOfInvalid;
m_type.m_metatype = MetaType::Precise;
return;
@@ -83,14 +102,14 @@ void TypeConstraint::init() {
Type dtype;
TRACE(5, "TypeConstraint: this %p type %s, nullable %d\n",
this, typeName->data(), nullable());
if (!mapGet(s_typeNamesToTypes, typeName, &dtype) ||
this, m_typeName->data(), nullable());
if (!mapGet(s_typeNamesToTypes, m_typeName, &dtype) ||
!(hhType() || dtype.m_dt == KindOfArray || dtype.isParent() ||
dtype.isSelf())) {
TRACE(5, "TypeConstraint: this %p no such type %s, treating as object\n",
this, typeName->data());
this, m_typeName->data());
m_type = { KindOfObject, MetaType::Precise };
m_namedEntity = Unit::GetNamedEntity(typeName);
m_namedEntity = Unit::GetNamedEntity(m_typeName);
TRACE(5, "TypeConstraint: NamedEntity: %p\n", m_namedEntity);
return;
}
@@ -144,7 +163,7 @@ bool TypeConstraint::checkTypedefObj(const TypedValue* tv) const {
bool
TypeConstraint::check(const TypedValue* tv, const Func* func) const {
assert(exists());
assert(hasConstraint());
// This is part of the interpreter runtime; perf matters.
if (tv->m_type == KindOfRef) {
@@ -210,6 +229,25 @@ TypeConstraint::checkPrimitive(DataType dt) const {
return equivDataTypes(m_type.m_dt, dt);
}
static const char* describe_actual_type(const TypedValue* tv) {
tv = tvToCell(tv);
switch (tv->m_type) {
case KindOfUninit:
case KindOfNull: return "null";
case KindOfBoolean: return "bool";
case KindOfInt64: return "int";
case KindOfDouble: return "double";
case KindOfStaticString:
case KindOfString: return "string";
case KindOfArray: return "array";
case KindOfObject:
return tv->m_data.pobj->o_getClassName().c_str();
default:
assert(false);
}
not_reached();
}
void TypeConstraint::verifyFail(const Func* func, int paramNum,
const TypedValue* tv) const {
Transl::VMRegAnchor _;
@@ -221,8 +259,23 @@ void TypeConstraint::verifyFail(const Func* func, int paramNum,
} else if (isParent()) {
parentToTypeName(func, &tn);
}
throw_unexpected_argument_type(paramNum + 1, fname.str().c_str(),
tn->data(), tvAsCVarRef(tv));
auto const givenType = describe_actual_type(tv);
if (isExtended()) {
// Extended type hints raise warnings instead of recoverable
// errors for now, to ease migration (we used to not check these
// at all at runtime).
assert(nullable() &&
"only nullable extended type hints are currently supported");
raise_warning(
"Argument %d to %s must be of type ?%s, %s given",
paramNum + 1, fname.str().c_str(), tn->data(), givenType);
} else {
raise_recoverable_error(
"Argument %d passed to %s must be an instance of %s, %s given",
paramNum + 1, fname.str().c_str(), tn->data(), givenType);
}
}
void TypeConstraint::selfToClass(const Func* func, const Class **cls) const {
+51 -15
Ver Arquivo
@@ -13,8 +13,8 @@
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#ifndef TYPE_CONSTRAINT_H_
#define TYPE_CONSTRAINT_H_
#ifndef incl_HPHP_TYPE_CONSTRAINT_H_
#define incl_HPHP_TYPE_CONSTRAINT_H_
#include <string>
#include <tr1/functional>
@@ -32,11 +32,27 @@ class TypeConstraint {
public:
enum Flags {
NoFlags = 0x0,
/*
* Nullable type hints check they are either the specified type,
* or null.
*/
Nullable = 0x1,
HHType = 0x2
/*
* This flag indicates either EnableHipHopSyntax was true, or the
* type came from a <?hh file and EnableHipHopSyntax was false.
*/
HHType = 0x2,
/*
* Extended hints are hints that do not apply to normal, vanilla
* php. For example "?Foo".
*/
ExtendedHint = 0x4,
};
protected:
private:
enum class MetaType {
Precise,
Self,
@@ -78,20 +94,26 @@ protected:
public:
void verifyFail(const Func* func, int paramNum, const TypedValue* tv) const;
explicit TypeConstraint(const StringData* typeName, Flags flags)
: m_flags(flags), m_typeName(typeName), m_namedEntity(0) {
TypeConstraint()
: m_flags(NoFlags)
, m_typeName(nullptr)
, m_namedEntity(nullptr)
{
init();
}
explicit TypeConstraint(const StringData* typeName = nullptr,
bool nullable = false, bool hhType = false)
: m_flags(NoFlags), m_typeName(typeName), m_namedEntity(0) {
if (nullable) m_flags = (Flags)(m_flags | Nullable);
if (hhType) m_flags = (Flags)(m_flags | HHType);
TypeConstraint(const StringData* typeName, Flags flags)
: m_flags(flags)
, m_typeName(typeName)
, m_namedEntity(nullptr)
{
init();
}
bool exists() const { return m_typeName; }
TypeConstraint(const TypeConstraint&) = default;
TypeConstraint& operator=(const TypeConstraint&) = default;
bool hasConstraint() const { return m_typeName; }
const StringData* typeName() const { return m_typeName; }
const NamedEntity* namedEntity() const { return m_namedEntity; }
@@ -124,12 +146,21 @@ public:
}
bool compat(const TypeConstraint& other) const {
if (other.isExtended() || isExtended()) {
/*
* Rely on the ahead of time typechecker---checking here can
* make it harder to convert a base class or interface to <?hh,
* because derived classes that are still <?php would all need
* to be modified.
*/
return true;
}
return (m_typeName == other.m_typeName
|| (m_typeName != nullptr && other.m_typeName != nullptr
&& m_typeName->isame(other.m_typeName)));
}
inline static bool equivDataTypes(DataType t1, DataType t2) {
static bool equivDataTypes(DataType t1, DataType t2) {
return
(t1 == t2) ||
(IS_STRING_TYPE(t1) && IS_STRING_TYPE(t2)) ||
@@ -158,12 +189,17 @@ public:
void selfToClass(const Func* func, const Class **cls) const;
void parentToClass(const Func* func, const Class **cls) const;
private:
bool isExtended() const { return m_flags & ExtendedHint; }
void selfToTypeName(const Func* func, const StringData **typeName) const;
void parentToTypeName(const Func* func, const StringData **typeName) const;
};
inline TypeConstraint::Flags
operator|(TypeConstraint::Flags a, TypeConstraint::Flags b) {
return TypeConstraint::Flags(static_cast<int>(a) | static_cast<int>(b));
}
}
#endif /* TYPE_CONSTRAINT_H_ */
#endif
-2
Ver Arquivo
@@ -2,8 +2,6 @@
class X<A,B,C> {}
function takes_string(string $x) { return "1\n"; }
function takes_x(X<int> $x) { return "1\n"; }
function takes_opt_string(?string $x) { return "2\n"; }
echo takes_string('foo');
echo takes_x(new X());
echo takes_opt_string(array(42)); // allowed -- maybe is desugared to nothing
echo takes_x("foo"); // not allowed -- should desugar to X
-1
Ver Arquivo
@@ -1,4 +1,3 @@
1
1
2
HipHop Fatal error: Argument 1 passed to takes_x() must be an instance of X, string given in %s on line 4
+12
Ver Arquivo
@@ -0,0 +1,12 @@
<?hh
class Bar { public function frob() { echo "frob\n"; } }
class Foo {}
function foo(?Bar $x) {
if ($x) {
$x->frob();
}
}
foo(new Foo);
+2
Ver Arquivo
@@ -0,0 +1,2 @@
HipHop Warning: Argument 1 to foo() must be of type ?Bar, Foo given in %s on line 10
HipHop Fatal error: Call to undefined method Foo::frob from anonymous context in %s on line 8
+13
Ver Arquivo
@@ -0,0 +1,13 @@
<?hh
interface Hey {
function wat(?Foo $x);
}
class Bar implements Hey {
public function wat($x) {
}
}
new Bar();
echo "ok\n";
+1
Ver Arquivo
@@ -0,0 +1 @@
ok
+11
Ver Arquivo
@@ -0,0 +1,11 @@
<?hh
class :url {
};
function foo(?:url $xhp_object): void {
}
foo(<url />);
foo(array(1,2,3));
foo(null);
+1
Ver Arquivo
@@ -0,0 +1 @@
HipHop Warning: Argument 1 to foo() must be of type ?xhp_url, array given in %s on line 7
+8
Ver Arquivo
@@ -0,0 +1,8 @@
<?hh
function foo(?(int, int) $x) {}
foo(null);
foo(array(1,2));
foo(array(1,2,3)); // ok: typechecker validates it
foo(new stdclass);
+1
Ver Arquivo
@@ -0,0 +1 @@
HipHop Warning: Argument 1 to foo() must be of type ?array, stdClass given in %s on line 3
+1
Ver Arquivo
@@ -0,0 +1 @@
../hiphop.hphp_opts
+1
Ver Arquivo
@@ -0,0 +1 @@
../hiphop.opts