Repository: VKCOM/noverify
Pull request #625 (2020-08-02 12:07:37)

linter: added type inference for arrays of complex types
```php
class Foo {}
$a = [new Foo(), new Foo()];
// exprtype($a, \Foo[]);
$b = [[new Foo(), new Foo()],[new Foo(), new Foo()]];
// exprtype($b, "\Foo[][]");
```

quasilyte: @YuriyNasretdinov as far as I understand, it doesn't continue to collect types if element types do mismatch.
So it always returns a map of 1 element (or false + nil map if it can't find that element type).

Patch:
diff --git a/src/solver/exprtype.go b/src/solver/exprtype.go
index 6f4cf0d..0a7e2e5 100644
--- a/src/solver/exprtype.go
+++ b/src/solver/exprtype.go
@@ -92,7 +92,7 @@ func internalFuncType(nm string, sc *meta.Scope, cs *meta.ClassParseState, c *ir
 	return meta.TypesMap{}, false
 }
 
-func arrayType(items []*ir.ArrayItemExpr) meta.TypesMap {
+func arrayType(sc *meta.Scope, cs *meta.ClassParseState, items []*ir.ArrayItemExpr) meta.TypesMap {
 	if len(items) == 0 {
 		// Used as a placeholder until more specific type is discovered.
 		//
@@ -106,48 +106,21 @@ func arrayType(items []*ir.ArrayItemExpr) meta.TypesMap {
 		return meta.NewTypesMap("empty_array")
 	}
 
-	if len(items) > 0 {
-		switch {
-		case isConstantStringArray(items):
-			return meta.NewTypesMap("string[]")
-		case isConstantIntArray(items):
-			return meta.NewTypesMap("int[]")
-		case isConstantFloatArray(items):
-			return meta.NewTypesMap("float[]")
-		}
-	}
-
-	return meta.NewTypesMap("mixed[]")
-}
-
-func isConstantStringArray(items []*ir.ArrayItemExpr) bool {
-	for _, item := range items {
-		if _, ok := item.Val.(*ir.String); !ok {
-			return false
-		}
-	}
+	firstElementType := ExprTypeLocal(sc, cs, items[0])
 
-	return true
-}
-
-func isConstantIntArray(items []*ir.ArrayItemExpr) bool {
-	for _, item := range items {
-		if _, ok := item.Val.(*ir.Lnumber); !ok {
-			return false
+	for _, item := range items[1:] {
+		itemType := ExprTypeLocal(sc, cs, item)
+		if !firstElementType.Equals(itemType) {
+			return meta.NewTypesMap("mixed[]")
 		}
 	}
 
-	return true
-}
-
-func isConstantFloatArray(items []*ir.ArrayItemExpr) bool {
-	for _, item := range items {
-		if _, ok := item.Val.(*ir.Dnumber); !ok {
-			return false
-		}
-	}
+	wrapped := meta.NewEmptyTypesMap(firstElementType.Len())
+	firstElementType.Iterate(func(t string) {
+		wrapped.AppendString(meta.WrapArrayOf(t))
+	})
 
-	return true
+	return wrapped
 }
 
 // CustomType specifies a mapping between some AST structure and concrete type (e.g. for <expr> instanceof <something>)
@@ -298,7 +271,9 @@ func exprTypeLocalCustom(sc *meta.Scope, cs *meta.ClassParseState, n ir.Node, cu
 	case *ir.ConcatExpr:
 		return meta.PreciseStringType
 	case *ir.ArrayExpr:
-		return arrayType(n.Items)
+		return arrayType(sc, cs, n.Items)
+	case *ir.ArrayItemExpr:
+		return ExprTypeLocalCustom(sc, cs, n.Val, custom)
 	case *ir.BooleanNotExpr, *ir.BooleanAndExpr, *ir.BooleanOrExpr,
 		*ir.EqualExpr, *ir.NotEqualExpr, *ir.IdenticalExpr, *ir.NotIdenticalExpr,
 		*ir.GreaterExpr, *ir.GreaterOrEqualExpr,


Test patch:
diff --git a/src/tests/checkers/oop_test.go b/src/tests/checkers/oop_test.go
index 6996c12..53de8e0 100644
--- a/src/tests/checkers/oop_test.go
+++ b/src/tests/checkers/oop_test.go
@@ -545,7 +545,7 @@ class C
 
 $b = new B();
 foreach ($b->create() as $item) {
-    $item->getC()->hello();
+    echo $item->getC()->hello();
 }
 `)
 }
diff --git a/src/tests/exprtype/exprtype_test.go b/src/tests/exprtype/exprtype_test.go
index 3154e41..6022c07 100644
--- a/src/tests/exprtype/exprtype_test.go
+++ b/src/tests/exprtype/exprtype_test.go
@@ -1807,7 +1807,7 @@ class Boo {}
  * @return \tuple(int, \Boo, int, string)
  */
 function foo() {
-    return [5, new Boo(), 10, "gas"];
+   return [5, new Boo(), 10, "gas"];
 }
 
 $tuple = foo();
@@ -1829,12 +1829,12 @@ exprtype($old_str, "string");
 
 
 class Bar {
-	/**
-	 * @return \tuple(int, \Boo, int, string)
-	 */
-	static function foo() {
-	    return [5, new Boo(), 10, "gas"];
-	}
+   /**
+    * @return \tuple(int, \Boo, int, string)
+    */
+   static function foo() {
+      return [5, new Boo(), 10, "gas"];
+   }
 }
 
 $class_static_tuple = Bar::foo();
@@ -1865,6 +1865,76 @@ exprtype($tuple_int_str, "unknown_from_list");
 	runExprTypeTest(t, &exprTypeTestParams{code: code})
 }
 
+func TestArrayTypes(t *testing.T) {
+	code := `<?php
+class Foo {}
+
+/** @return Foo */
+function f() {}
+/** @return float */
+function return_float() {}
+/** @return int */
+function return_int() {}
+/** @return string */
+function return_string() {}
+
+
+$one_dimensional = [new Foo(), new Foo()];
+exprtype($one_dimensional, "\Foo[]");
+
+
+$two_dimensional = [[new Foo(), new Foo()],[new Foo(), new Foo()]];
+exprtype($two_dimensional, "\Foo[][]");
+
+
+$three_dimensional = [[[new Foo(), new Foo()],[new Foo(), new Foo()]],[[new Foo(), new Foo()],[new Foo(), new Foo()]]];
+exprtype($three_dimensional, "\Foo[][][]");
+
+
+$a = [10, 20, 30];
+exprtype($a, "int[]");
+$a = [return_int(), return_int(), return_int()];
+exprtype($a, "int[]");
+// but
+$a = [return_int(), 1];
+exprtype($a, "mixed[]");
+
+
+$a = [10.5, 20.5, 30.5];
+exprtype($a, "float[]");
+$a = [return_float(), return_float(), return_float()];
+exprtype($a, "float[]");
+// but
+$a = [return_float(), 12.5];
+exprtype($a, "mixed[]");
+
+
+$a = ["Hello", "World", "!"];
+exprtype($a, "string[]");
+$a = [return_string(), return_string(), return_string()];
+exprtype($a, "string[]");
+// but
+$a = [return_string(), "World!"];
+exprtype($a, "mixed[]");
+
+
+$a = [f(), f()];
+exprtype($a, "\Foo[]");
+// but
+$a = [f(), new Foo()];
+exprtype($a, "mixed[]");
+
+
+$a = [f(), g()];
+exprtype($a, "mixed[]");
+
+
+$a = [];
+exprtype($a, "mixed[]");
+`
+	runExprTypeTest(t, &exprTypeTestParams{code: code})
+}
+
 func TestPropertyTypeHints(t *testing.T) {
 	code := `<?php
 class Too {}