Repository: VKCOM/noverify
Pull request #614 (2020-07-28 13:19:58)

Infer types in list() over tuple
Added type inference for each variable when destructuring a variable with the `\tuple` type or a function that returns `\tuple`.
Added test.

For example:
```php
class Boo {}

/**
 * @return \tuple(int, \Boo, int, string)
 */
function foo() {
    return [5, new Boo(), 10, "gas"];
}

$tuple = foo();

[$i, $bo, $j, $str] = $tuple;

exprtype($i, "int");
exprtype($bo, "\Boo");
exprtype($j, "int");
exprtype($str, "string");
```


Patch:
diff --git a/src/linter/block.go b/src/linter/block.go
index fbfca0d..eea5f7f 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -2196,9 +2196,22 @@ func (b *BlockWalker) handleAssignReference(a *assign.Reference) bool {
 	return false
 }
 
-func (b *BlockWalker) handleAssignList(items []*expr.ArrayItem) {
-	for _, item := range items {
-		b.handleVariableNode(item.Val, meta.NewTypesMap("unknown_from_list"), "assign")
+func (b *BlockWalker) handleAssignList(items []*expr.ArrayItem, info meta.ClassInfo, isShape bool) {
+	if isShape {
+		for i, item := range items {
+			prop, ok := info.Properties[fmt.Sprint(i)]
+			var tp meta.TypesMap
+			if !ok {
+				tp = meta.NewTypesMap("unknown_from_list")
+			} else {
+				tp = prop.Typ
+			}
+			b.handleVariableNode(item.Val, tp, "assign")
+		}
+	} else {
+		for _, item := range items {
+			b.handleVariableNode(item.Val, meta.NewTypesMap("unknown_from_list"), "assign")
+		}
 	}
 }
 
@@ -2302,7 +2315,27 @@ func (b *BlockWalker) handleAssign(a *assign.Assign) bool {
 		b.checkVoidType(a.Expression)
 		b.replaceVar(v, solver.ExprTypeLocal(b.ctx.sc, b.r.ctx.st, a.Expression), "assign", meta.VarAlwaysDefined)
 	case *expr.List:
-		b.handleAssignList(v.Items)
+		if !meta.IsIndexingComplete() {
+			return true
+		}
+
+		tp := solver.ExprType(b.ctx.sc, b.r.ctx.st, a.Expression)
+		var shapeType string
+		tp.Iterate(func(t string) {
+			if strings.HasPrefix(t, `\shape$`) {
+				shapeType = t
+			}
+		})
+
+		var class meta.ClassInfo
+		var ok bool
+		var isShape bool
+		if shapeType != "" {
+			class, ok = meta.Info.GetClass(shapeType)
+			isShape = ok
+		}
+
+		b.handleAssignList(v.Items, class, isShape)
 	case *expr.PropertyFetch:
 		v.Property.Walk(b)
 		sv, ok := v.Variable.(*node.SimpleVar)


Test patch:
diff --git a/src/tests/exprtype/exprtype_test.go b/src/tests/exprtype/exprtype_test.go
index 08170c5..6682c5f 100644
--- a/src/tests/exprtype/exprtype_test.go
+++ b/src/tests/exprtype/exprtype_test.go
@@ -1805,6 +1805,72 @@ exprtype(--$a, "float");
 	runExprTypeTest(t, &exprTypeTestParams{code: code})
 }
 
+func TestTypesListOverTuple(t *testing.T) {
+	code := `<?php
+class Boo {}
+
+/**
+ * @return \tuple(int, \Boo, int, string)
+ */
+function foo() {
+    return [5, new Boo(), 10, "gas"];
+}
+
+$tuple = foo();
+
+[$i, $bo, $j, $str] = $tuple; // With short syntax
+
+exprtype($i, "int");
+exprtype($bo, "\Boo");
+exprtype($j, "int");
+exprtype($str, "string");
+
+
+list($old_i, $old_bo, $old_j, $old_str) = $tuple; // With old syntax
+
+exprtype($old_i, "int");
+exprtype($old_bo, "\Boo");
+exprtype($old_j, "int");
+exprtype($old_str, "string");
+
+
+class Bar {
+	/**
+	 * @return \tuple(int, \Boo, int, string)
+	 */
+	static function foo() {
+	    return [5, new Boo(), 10, "gas"];
+	}
+}
+
+$class_static_tuple = Bar::foo();
+
+[$class_static_i, $class_static_bo, $class_static_j, $class_static_str] = $class_static_tuple;
+
+exprtype($class_static_i, "int");
+exprtype($class_static_bo, "\Boo");
+exprtype($class_static_j, "int");
+exprtype($class_static_str, "string");
+
+
+[$function_i, $function_bo, $function_j, $function_str] = Bar::foo();
+
+exprtype($function_i, "int");
+exprtype($function_bo, "\Boo");
+exprtype($function_j, "int");
+exprtype($function_str, "string");
+
+$tuple_int = 10;
+[$tuple_int_i, $tuple_int_foo, $tuple_int_j, $tuple_int_str] = $tuple_int;
+
+exprtype($tuple_int_i, "unknown_from_list");
+exprtype($tuple_int_foo, "unknown_from_list");
+exprtype($tuple_int_j, "unknown_from_list");
+exprtype($tuple_int_str, "unknown_from_list");
+`
+	runExprTypeTest(t, &exprTypeTestParams{code: code})
+}
+
 func runExprTypeTest(t *testing.T, params *exprTypeTestParams) {
 	meta.ResetInfo()
 	if params.stubs != "" {