Repository: VKCOM/noverify
Pull request #859 (2020-12-14 16:47:03)

solver: added initial support for instance_deserialize in KPHP mode
At the moment, string assignment of the class
and assignment through the class constant are
supported:

```php
instance_deserialize ("", Foo::class);
instance_deserialize ("", "Foo");
```


Patch:
diff --git a/src/meta/metainfo.go b/src/meta/metainfo.go
index 2f412b0..b6ab1fb 100644
--- a/src/meta/metainfo.go
+++ b/src/meta/metainfo.go
@@ -144,6 +144,12 @@ func (i *info) InitKphpStubs() {
 		MinParamsCnt: 1,
 		Typ:          NewTypesMap("mixed"),
 	}
+	internalFunctions.H[`\instance_deserialize`] = FuncInfo{
+		Name:         `\instance_deserialize`,
+		Params:       []FuncParam{{Name: "packed_str"}, {Name: "type_of_instance"}},
+		MinParamsCnt: 2,
+		Typ:          NewTypesMap("object|null"),
+	}
 
 	internalFunctionOverrides[`\array_first_element`] = FuncInfoOverride{
 		OverrideType: OverrideElementType,
@@ -153,6 +159,10 @@ func (i *info) InitKphpStubs() {
 		OverrideType: OverrideElementType,
 		ArgNum:       0,
 	}
+	internalFunctionOverrides[`\instance_deserialize`] = FuncInfoOverride{
+		OverrideType: OverrideClassType,
+		ArgNum:       1,
+	}
 }
 
 func (i *info) InitStubs() {
@@ -354,6 +364,8 @@ const (
 	OverrideArgType OverrideType = iota
 	// OverrideElementType means that return type of a function is the same as the type of an element of the argument
 	OverrideElementType
+	// OverrideClassType means that return type of a function is the same as the type represented by the class name.
+	OverrideClassType
 )
 
 type AccessLevel int
diff --git a/src/solver/exprtype.go b/src/solver/exprtype.go
index ccefe55..0a558ba 100644
--- a/src/solver/exprtype.go
+++ b/src/solver/exprtype.go
@@ -202,6 +202,39 @@ func binaryPlusOpType(sc *meta.Scope, cs *meta.ClassParseState, left, right ir.N
 	return binaryMathOpType(sc, cs, left, right, custom)
 }
 
+func classNameToString(cs *meta.ClassParseState, n ir.Node) (string, bool) {
+	var name string
+
+	switch n := n.(type) {
+	case *ir.String:
+		name = n.Value
+	case *ir.ClassConstFetchExpr:
+		if !strings.EqualFold(n.ConstantName.Value, "class") {
+			return "", false
+		}
+
+		switch class := n.Class.(type) {
+		case *ir.Name:
+			name = class.Value
+		case *ir.Identifier:
+			name = class.Value
+		case *ir.SimpleVar:
+			name = "$" + class.Name
+		default:
+			return "", false
+		}
+	default:
+		return "", false
+	}
+
+	className, ok := GetClassName(cs, &ir.Name{Value: name})
+	if !ok {
+		return "", false
+	}
+
+	return className, true
+}
+
 func internalFuncType(nm string, sc *meta.Scope, cs *meta.ClassParseState, c *ir.FunctionCallExpr, custom []CustomType) (typ meta.TypesMap, ok bool) {
 	fn, ok := meta.GetInternalFunctionInfo(nm)
 	if !ok || fn.Typ.IsEmpty() {
@@ -215,14 +248,27 @@ func internalFuncType(nm string, sc *meta.Scope, cs *meta.ClassParseState, c *ir
 
 	arg := c.Arg(override.ArgNum)
 	typ = ExprTypeLocalCustom(sc, cs, arg.Expr, custom)
-	if override.OverrideType == meta.OverrideArgType {
+
+	switch override.OverrideType {
+	case meta.OverrideArgType:
 		return typ, true
-	} else if override.OverrideType == meta.OverrideElementType {
+
+	case meta.OverrideElementType:
 		newTyp := meta.NewEmptyTypesMap(typ.Len())
 		typ.Iterate(func(t string) {
 			newTyp = newTyp.AppendString(meta.WrapElemOf(t))
 		})
 		return newTyp, true
+
+	case meta.OverrideClassType:
+		// due to the fact that it is impossible for us to use constfold
+		// here, we have to process only a part of the possible options,
+		// although the most popular ones.
+		className, ok := classNameToString(cs, arg.Expr)
+		if !ok {
+			return meta.NewTypesMap("mixed"), true
+		}
+		return meta.NewTypesMap(className + "|null"), true
 	}
 
 	log.Printf("Internal error: unexpected override type %d for function %s", override.OverrideType, nm)


Test patch:
diff --git a/src/tests/exprtype/kphp_test.go b/src/tests/exprtype/kphp_test.go
index c4c173a..bd6f705 100644
--- a/src/tests/exprtype/kphp_test.go
+++ b/src/tests/exprtype/kphp_test.go
@@ -24,6 +24,39 @@ exprtype(get_any_arr(), 'int[][]|mixed[][]');
 	runKPHPExprTypeTest(t, &exprTypeTestParams{code: code})
 }
 
+func TestInstanceDeserializeType(t *testing.T) {
+	code := `<?php
+class Foo {
+	/** Method */
+	public function method() {}
+}
+
+const CLASS_NAME = "Foo";
+
+class Boo {
+	/** Method */
+	public function method() {
+		$text = "";
+		exprtype(instance_deserialize($text, self::class), "\Boo|null");
+		exprtype(instance_deserialize($text, static::class), "\Boo|null");
+		exprtype(instance_deserialize($text, $this::class), "\Boo|null");
+	}
+}
+
+function f() {
+	$text = "";
+	$className = "";
+
+	exprtype(instance_deserialize($text, Foo::class), "\Foo|null");
+	exprtype(instance_deserialize($text, "Foo"), "\Foo|null");
+	exprtype(instance_deserialize($text, 10), "mixed");
+	exprtype(instance_deserialize($text, $className), "mixed");
+	exprtype(instance_deserialize($text, CLASS_NAME), "mixed");
+}
+`
+	runKPHPExprTypeTest(t, &exprTypeTestParams{code: code, stubs: "<?php /* no code */"})
+}
+
 func runKPHPExprTypeTest(t *testing.T, params *exprTypeTestParams) {
 	linter.KPHP = true
 	runExprTypeTest(t, params)