Repository: VKCOM/noverify
Pull request #660 (2020-08-11 21:23:02)

linter: infer types in list() over shape
Added type inference for each variable when destructuring variable with the `\shape` type or function that returns `\shape`.

For example:
```php
/** @return shape(key:\Foo, key2:float, key3:int) */
function asShapeWithStringKey() { return []; }

function foo() {
  ["key" => $a, "key2" => $b, "key3" => $c] = asShapeWithStringKey();

  exprtype($a, "\Foo");
  exprtype($b, "float");
  exprtype($c, "int");
}
```


Patch:
diff --git a/src/linter/block.go b/src/linter/block.go
index a134608..0c06a4f 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -1604,18 +1604,42 @@ func (b *BlockWalker) handleAssignReference(a *ir.AssignReference) bool {
 	return false
 }
 
-func (b *BlockWalker) handleAssignList(items []*ir.ArrayItemExpr, 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
+func (b *BlockWalker) handleAssignShapeToList(items []*ir.ArrayItemExpr, info meta.ClassInfo) {
+	for i, item := range items {
+		var prop meta.PropertyInfo
+		var ok bool
+
+		if item.Key != nil {
+			var key string
+			switch keyNode := item.Key.(type) {
+			case *ir.String:
+				key = unquote(keyNode.Value)
+			case *ir.Lnumber:
+				key = keyNode.Value
+			case *ir.Dnumber:
+				key = keyNode.Value
+			}
+
+			if key != "" {
+				prop, ok = info.Properties[key]
 			}
-			b.handleVariableNode(item.Val, tp, "assign")
+		} else {
+			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")
+	}
+}
+
+func (b *BlockWalker) handleAssignList(items []*ir.ArrayItemExpr, info meta.ClassInfo, isShape bool) {
+	if isShape {
+		b.handleAssignShapeToList(items, info)
 	} else {
 		for _, item := range items {
 			b.handleVariableNode(item.Val, meta.NewTypesMap("unknown_from_list"), "assign")


Test patch:
diff --git a/src/tests/exprtype/exprtype_test.go b/src/tests/exprtype/exprtype_test.go
index fa5e918..e669aff 100644
--- a/src/tests/exprtype/exprtype_test.go
+++ b/src/tests/exprtype/exprtype_test.go
@@ -1865,6 +1865,89 @@ exprtype($tuple_int_str, "unknown_from_list");
 	runExprTypeTest(t, &exprTypeTestParams{code: code})
 }
 
+func TestTypesShapeOverList(t *testing.T) {
+	code := `<?php
+class Foo {}
+
+/** @return shape(key:\Foo, key2:float, key3:int) */
+function asShapeWithStringKey() { return []; }
+
+/** @return shape(0:\Foo, 2:float, 1:int) */
+function asShapeWithIntKey() { return []; }
+
+/** @return shape(0:\Foo, 4:float, 2:int) */
+function asShapeWithSomeIntKey() { return []; }
+
+function foo() {
+  // simple
+  ["key" => $a1, "key2" => $b1, "key3" => $c1] = asShapeWithStringKey();
+
+  exprtype($a1, "\Foo");
+  exprtype($b1, "float");
+  exprtype($c1, "int");
+
+
+  // mixed positions
+  ["key" => $a2, "key3" => $c2, "key2" => $b2] = asShapeWithStringKey();
+
+  exprtype($a2, "\Foo");
+  exprtype($b2, "float");
+  exprtype($c2, "int");
+
+
+  // without keys and shape with string key
+  [$a3, $c3, $b3] = asShapeWithStringKey();
+
+  exprtype($a3, "unknown_from_list");
+  exprtype($b3, "unknown_from_list");
+  exprtype($c3, "unknown_from_list");
+
+
+  // without keys and shape with int key
+  [$a4, $b4, $c4] = asShapeWithIntKey();
+
+  exprtype($a4, "\Foo");
+  exprtype($b4, "int");
+  exprtype($c4, "float");
+
+
+  // without keys and shape with some int key
+  [$a5, $b5, $c5, $d5, $e5] = asShapeWithSomeIntKey();
+
+  exprtype($a5, "\Foo");
+  exprtype($b5, "unknown_from_list");
+  exprtype($c5, "int");
+  exprtype($d5, "unknown_from_list");
+  exprtype($e5, "float");
+
+
+  // with keys and shape with some int key
+  [0 => $a6, 2 => $b6, 4 => $c6] = asShapeWithSomeIntKey();
+
+  exprtype($a6, "\Foo");
+  exprtype($b6, "int");
+  exprtype($c6, "float");
+
+
+  // with old style and keys and shape with some int key
+  list(0 => $a7, 2 => $b7, 4 => $c7) = asShapeWithSomeIntKey();
+
+  exprtype($a7, "\Foo");
+  exprtype($b7, "int");
+  exprtype($c7, "float");
+
+
+  // with old style and without keys and shape with string key
+  list($a8, $c8, $b8) = asShapeWithStringKey();
+
+  exprtype($a8, "unknown_from_list");
+  exprtype($b8, "unknown_from_list");
+  exprtype($c8, "unknown_from_list");
+}
+`
+	runExprTypeTest(t, &exprTypeTestParams{code: code})
+}
+
 func TestArrayTypes(t *testing.T) {
 	code := `<?php
 class Foo {}