Repository: VKCOM/noverify
Pull request #690 (2020-08-19 13:14:14)

linter: added the ability to type inference for arguments to a callback function when calling a function with this callback
Supported functions:
- `\array_map`
- `\array_walk`
- `\array_walk_recursive`
- `\array_filter`
- `\array_reduce`
- `\usort`
- `\uasort`


Patch:
diff --git a/src/linter/block.go b/src/linter/block.go
index 438faab..27d8159 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -282,7 +282,7 @@ func (b *BlockWalker) EnterNode(n ir.Node) (res bool) {
 		if isInstance {
 			typ, _ = b.ctx.sc.GetVarNameType("this")
 		}
-		res = b.enterClosure(s, isInstance, typ)
+		res = b.enterClosure(s, isInstance, typ, nil)
 
 	case *ir.ReturnStmt:
 		b.handleReturn(s)
@@ -718,6 +718,26 @@ func (b *BlockWalker) handleCallArgs(n ir.Node, args []ir.Node, fn meta.FuncInfo
 				break
 			}
 			a.Walk(b)
+		case *ir.ClosureExpr:
+			var typ meta.TypesMap
+			isInstance := b.ctx.sc.IsInInstanceMethod()
+			if isInstance {
+				typ, _ = b.ctx.sc.GetVarNameType("this")
+			}
+
+			// find the types for the arguments of the function that contains this closure
+			var funcArgTypes []meta.TypesMap
+			for _, arg := range args {
+				tp := solver.ExprTypeLocal(b.ctx.sc, b.r.ctx.st, arg.(*ir.Argument).Expr)
+				funcArgTypes = append(funcArgTypes, tp)
+			}
+
+			closureSolver := &solver.ClosureCallerInfo{
+				Name:     fn.Name,
+				ArgTypes: funcArgTypes,
+			}
+
+			b.enterClosure(a, isInstance, typ, closureSolver)
 		default:
 			a.Walk(b)
 		}
@@ -1152,7 +1172,7 @@ func (b *BlockWalker) handleFor(s *ir.ForStmt) bool {
 	return false
 }
 
-func (b *BlockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType meta.TypesMap) bool {
+func (b *BlockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType meta.TypesMap, closureSolver *solver.ClosureCallerInfo) bool {
 	sc := meta.NewScope()
 	sc.SetInClosure(true)
 
@@ -1193,7 +1213,7 @@ func (b *BlockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 		b.untrackVarName(v.Name)
 	}
 
-	params, _ := b.r.parseFuncArgs(fun.Params, phpDocParamTypes, sc)
+	params, _ := b.r.parseFuncArgs(fun.Params, phpDocParamTypes, sc, closureSolver)
 
 	b.r.handleFuncStmts(params, closureUses, fun.Stmts, sc)
 	b.r.addScope(fun, sc)
diff --git a/src/linter/root.go b/src/linter/root.go
index 153cd90..0a4a1f5 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -1007,7 +1007,7 @@ func (d *RootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	phpDocParamTypes := doc.types
 
 	class := d.getClass()
-	params, minParamsCnt := d.parseFuncArgs(meth.Params, phpDocParamTypes, sc)
+	params, minParamsCnt := d.parseFuncArgs(meth.Params, phpDocParamTypes, sc, nil)
 
 	if len(class.Interfaces) != 0 {
 		// If we implement interfaces, methods that take a part in this
@@ -1362,12 +1362,68 @@ func (d *RootWalker) parseTypeNode(n ir.Node) (typ meta.TypesMap, ok bool) {
 	return tm, !tm.IsEmpty()
 }
 
-func (d *RootWalker) parseFuncArgs(params []ir.Node, parTypes phpDocParamsMap, sc *meta.Scope) (args []meta.FuncParam, minArgs int) {
+func (d *RootWalker) callbackParamByIndex(param ir.Node, argType meta.TypesMap) meta.FuncParam {
+	p := param.(*ir.Parameter)
+	v := p.Variable
+	var typ meta.TypesMap
+	argType.Iterate(func(t string) {
+		typ = typ.AppendString(meta.WrapElemOf(t))
+	})
+	arg := meta.FuncParam{
+		IsRef: p.ByRef,
+		Name:  v.Name,
+		Typ:   typ,
+	}
+	return arg
+}
+
+func (d *RootWalker) parseFuncArgsForCallback(params []ir.Node, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (args []meta.FuncParam, minArgs int) {
+	countParams := len(params)
+	minArgs = countParams
+	if countParams == 0 {
+		return nil, 0
+	}
+	args = make([]meta.FuncParam, countParams)
+
+	switch closureSolver.Name {
+	case `\usort`, `\uasort`, `\array_reduce`:
+		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[0])
+		if countParams > 1 {
+			args[1] = d.callbackParamByIndex(params[1], closureSolver.ArgTypes[0])
+		}
+	case `\array_walk`, `\array_walk_recursive`, `\array_filter`:
+		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[0])
+	case `\array_map`:
+		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[1])
+	}
+
+	for i, param := range params {
+		p := param.(*ir.Parameter)
+		v := p.Variable
+		var typ meta.TypesMap
+		if i < len(args) {
+			typ = args[i].Typ
+		} else {
+			typ = meta.MixedType
+		}
+
+		sc.AddVarName(v.Name, typ, "param", meta.VarAlwaysDefined)
+	}
+
+	return args, minArgs
+}
+
+func (d *RootWalker) parseFuncArgs(params []ir.Node, parTypes phpDocParamsMap, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (args []meta.FuncParam, minArgs int) {
 	if len(params) == 0 {
 		return nil, 0
 	}
 
 	args = make([]meta.FuncParam, 0, len(params))
+
+	if closureSolver != nil && solver.IsSupportedFunction(closureSolver.Name) {
+		return d.parseFuncArgsForCallback(params, sc, closureSolver)
+	}
+
 	for _, param := range params {
 		p := param.(*ir.Parameter)
 		v := p.Variable
@@ -1482,7 +1538,7 @@ func (d *RootWalker) enterFunction(fun *ir.FunctionStmt) bool {
 
 	sc := meta.NewScope()
 
-	params, minParamsCnt := d.parseFuncArgs(fun.Params, phpDocParamTypes, sc)
+	params, minParamsCnt := d.parseFuncArgs(fun.Params, phpDocParamTypes, sc, nil)
 
 	funcInfo := d.handleFuncStmts(params, nil, fun.Stmts, sc)
 	actualReturnTypes := funcInfo.returnTypes
diff --git a/src/solver/closure.go b/src/solver/closure.go
new file mode 100644
index 0000000..6b5f280
--- /dev/null
+++ b/src/solver/closure.go
@@ -0,0 +1,26 @@
+package solver
+
+import (
+	"github.com/VKCOM/noverify/src/meta"
+)
+
+var supportedFunctions = map[string]struct{}{
+	`\array_map`:            {},
+	`\array_walk`:           {},
+	`\array_walk_recursive`: {},
+	`\array_filter`:         {},
+	`\array_reduce`:         {},
+	`\usort`:                {},
+	`\uasort`:               {},
+}
+
+func IsSupportedFunction(name string) bool {
+	_, ok := supportedFunctions[name]
+	return ok
+}
+
+// Struct containing information about the function that called the closure.
+type ClosureCallerInfo struct {
+	Name     string          // caller function name
+	ArgTypes []meta.TypesMap // types for each arg for call caller function
+}


Test patch:
diff --git a/src/tests/exprtype/exprtype_test.go b/src/tests/exprtype/exprtype_test.go
index dcea957..60a5988 100644
--- a/src/tests/exprtype/exprtype_test.go
+++ b/src/tests/exprtype/exprtype_test.go
@@ -2159,6 +2159,203 @@ exprtype(Roo::$d, "string");
 	runExprTypeTest(t, &exprTypeTestParams{code: code})
 }
 
+func TestClosureCallbackArgumentsTypes(t *testing.T) {
+	code := `<?php
+function usort($array, $callback) {}
+function uasort($array, $callback) {}
+function array_map($callback, $array, $_ = null) {}
+function array_walk($callback, $array) {}
+function array_walk_recursive($callback, $array) {}
+function array_filter($array, $callback) {}
+function array_reduce($array, $callback) {}
+function some_function_without_model($callback, $array) {}
+
+class Foo { public function f() {} }
+class Boo { public function b() {} }
+
+/** @return Foo[] */
+function return_foo() { return []; }
+
+/** @return Boo[] */
+function return_boo() { return []; }
+
+$foo_array = return_foo();
+$boo_array = return_boo();
+
+usort($foo_array, function($a, $b) {
+  exprtype($a, "\Foo");
+  exprtype($b, "\Foo");
+});
+
+uasort($foo_array, function($a, $b) {
+  exprtype($a, "\Foo");
+  exprtype($b, "\Foo");
+});
+
+array_map(function($a) {
+  exprtype($a, "\Foo");
+}, $foo_array);
+
+array_walk($foo_array, function($a, $b) {
+  exprtype($a, "\Foo");
+  exprtype($b, "mixed");
+});
+
+// wrong number of args
+array_walk($foo_array, function($a, $b, $c) {
+  exprtype($a, "\Foo");
+  exprtype($b, "mixed");
+  exprtype($c, "mixed");
+});
+
+array_walk_recursive($foo_array, function($a, $b) {
+  exprtype($a, "\Foo");
+  exprtype($b, "mixed");
+});
+
+array_filter($foo_array, function($a) {
+  exprtype($a, "\Foo");
+});
+
+array_reduce($foo_array, function($carry, $item) {
+  exprtype($carry, "\Foo");
+  exprtype($item, "\Foo");
+});
+
+some_function_without_model(function($b) {
+  exprtype($b, "mixed");
+}, $d);
+
+// Not supported
+function callback($a) {
+  exprtype($a, "mixed");
+}
+
+// Not supported
+$callback = function($a) {
+  exprtype($a, "mixed");
+};
+
+array_map($callback, $d);
+array_map('callback', $d);
+`
+	runExprTypeTest(t, &exprTypeTestParams{code: code})
+}
+
+func TestClosureCallbackArgumentsPossibleErrorVariations(t *testing.T) {
+	code := `<?php
+function usort($array, $callback) {}
+function uasort($array, $callback) {}
+function array_map($callback, $array, $_ = null) {}
+function array_walk($callback, $array) {}
+function array_walk_recursive($callback, $array) {}
+function array_filter($array, $callback) {}
+function array_reduce($array, $callback) {}
+
+class Foo { public function f() {} }
+
+/** @return Foo[] */
+function return_foo() { return []; }
+
+$foo_array = return_foo();
+
+// more arguments than necessary
+usort($foo_array, function($a, $b, $c) {
+  exprtype($a, "\Foo");
+  exprtype($b, "\Foo");
+  exprtype($c, "mixed");
+});
+
+// less arguments than necessary
+uasort($foo_array, function($a) {
+  exprtype($a, "\Foo");
+});
+
+// more arguments than necessary
+array_map(function($a, $b) {
+  exprtype($a, "\Foo");
+  exprtype($b, "mixed");
+}, $foo_array);
+
+// less arguments than necessary
+array_map(function() {
+
+}, $foo_array);
+
+// more arguments than necessary
+array_walk($foo_array, function($a, $b, $c) {
+  exprtype($a, "\Foo");
+  exprtype($b, "mixed");
+  exprtype($c, "mixed");
+});
+
+// less arguments than necessary
+array_walk($foo_array, function() {
+});
+
+// more arguments than necessary
+array_filter($foo_array, function($a, $b) {
+  exprtype($a, "\Foo");
+  exprtype($b, "mixed");
+});
+
+// less arguments than necessary
+array_filter($foo_array, function() {
+});
+
+// more arguments than necessary
+array_reduce($foo_array, function($carry, $item, $c) {
+  exprtype($carry, "\Foo");
+  exprtype($item, "\Foo");
+  exprtype($c, "mixed");
+});
+
+// less arguments than necessary
+array_reduce($foo_array, function() {
+});
+`
+	runExprTypeTest(t, &exprTypeTestParams{code: code})
+}
+
+func TestNestedClosureCallback(t *testing.T) {
+	code := `<?php
+function array_map($callback, array $arr1, array $_ = null) {}
+function usort($array, $callback) {}
+
+class Foo { public function f() {} }
+/** @return Foo[][] */
+
+function return_foo() { return []; }
+
+$foo_array = return_foo();
+
+array_map(function($a) {
+  exprtype($a, "\Foo[]");
+  array_map(function($a) {
+    exprtype($a, "\Foo");
+  }, $a);
+  exprtype($a, "\Foo[]");
+}, $foo_array);
+
+usort($foo_array, function($a, $b) {
+  exprtype($a, "\Foo[]");
+  exprtype($b, "\Foo[]");
+
+  array_map(function($a) {
+    exprtype($a, "\Foo");
+  }, $a);
+
+  array_map(function($b) {
+    exprtype($b, "\Foo");
+  }, $b);
+
+  exprtype($a, "\Foo[]");
+  exprtype($b, "\Foo[]");
+});
+`
+	runExprTypeTest(t, &exprTypeTestParams{code: code})
+}
+
 func runExprTypeTest(t *testing.T, params *exprTypeTestParams) {
 	meta.ResetInfo()
 	if params.stubs != "" {