Repository: VKCOM/noverify
Pull request #976 (2021-04-15 10:51:59)

meta: add type inferring for not_null, not_false and create_vector from polyfills
Since type inference occurs before lazy types are resolved, 
this does not work entirely for function return types.

Perhaps we should add the ability to add a lazy type, which 
would say that when resolving types, need to remove a certain 
type, for example, `false` or `null`.

#977

quasilyte: Conflicts.
i582: > Conflicts.

Fixed.
i582: Oh no...
quasilyte: :sweat_smile: 

Patch:
diff --git a/src/meta/info.go b/src/meta/info.go
index d30f1de..52bf84b 100644
--- a/src/meta/info.go
+++ b/src/meta/info.go
@@ -184,6 +184,25 @@ func (i *Info) InitKphpStubs() {
 		Typ:          types.NewMap("object"),
 	}
 
+	i.internalFunctions.H[`\not_null`] = FuncInfo{
+		Name:         `\not_null`,
+		Params:       []FuncParam{{Name: "any_value"}},
+		MinParamsCnt: 1,
+		Typ:          types.NewMap("mixed"),
+	}
+	i.internalFunctions.H[`\not_false`] = FuncInfo{
+		Name:         `\not_false`,
+		Params:       []FuncParam{{Name: "any_value"}},
+		MinParamsCnt: 1,
+		Typ:          types.NewMap("mixed"),
+	}
+	i.internalFunctions.H[`\create_vector`] = FuncInfo{
+		Name:         `\create_vector`,
+		Params:       []FuncParam{{Name: "count"}, {Name: "el"}},
+		MinParamsCnt: 2,
+		Typ:          types.NewMap("mixed[]"),
+	}
+
 	i.internalFunctionOverrides[`\array_first_value`] = FuncInfoOverride{
 		OverrideType: OverrideElementType,
 		ArgNum:       0,
@@ -208,6 +227,22 @@ func (i *Info) InitKphpStubs() {
 		OverrideType: OverrideNullableClassType,
 		ArgNum:       0,
 	}
+
+	i.internalFunctionOverrides[`\not_null`] = FuncInfoOverride{
+		OverrideType: OverrideArgType,
+		Properties:   NotNull,
+		ArgNum:       0,
+	}
+	i.internalFunctionOverrides[`\not_false`] = FuncInfoOverride{
+		OverrideType: OverrideArgType,
+		Properties:   NotFalse,
+		ArgNum:       0,
+	}
+	i.internalFunctionOverrides[`\create_vector`] = FuncInfoOverride{
+		OverrideType: OverrideArgType,
+		Properties:   ArrayOf,
+		ArgNum:       1,
+	}
 }
 
 func (i *Info) InitStubs() {
diff --git a/src/meta/metainfo.go b/src/meta/metainfo.go
index 955887f..cd4bcff 100644
--- a/src/meta/metainfo.go
+++ b/src/meta/metainfo.go
@@ -83,10 +83,22 @@ const (
 	OverrideNullableClassType
 )
 
+type OverrideProperties int
+
+const (
+	// NotNull means that the null type will be removed from the resulting type.
+	NotNull OverrideProperties = 1 << iota
+	// NotFalse means that the false type will be removed from the resulting type.
+	NotFalse
+	// ArrayOf means that the type will be converted to an array of elements of that type.
+	ArrayOf
+)
+
 // FuncInfoOverride defines return type overrides based on their parameter types.
 // For example, \array_slice($arr) returns type of element (OverrideElementType) of the ArgNum=0
 type FuncInfoOverride struct {
 	OverrideType OverrideType
+	Properties   OverrideProperties
 	ArgNum       int
 }
 
diff --git a/src/solver/exprtype.go b/src/solver/exprtype.go
index 7a9f4c5..e9b4cd3 100644
--- a/src/solver/exprtype.go
+++ b/src/solver/exprtype.go
@@ -252,11 +252,10 @@ func internalFuncType(nm string, sc *meta.Scope, cs *meta.ClassParseState, c *ir
 
 	switch override.OverrideType {
 	case meta.OverrideArgType:
-		return typ, true
+		// do nothing
 
 	case meta.OverrideElementType:
-		newTyp := typ.Map(types.WrapElemOf)
-		return newTyp, true
+		typ = typ.Map(types.WrapElemOf)
 
 	case meta.OverrideClassType, meta.OverrideNullableClassType:
 		// due to the fact that it is impossible for us to use constfold
@@ -268,14 +267,29 @@ func internalFuncType(nm string, sc *meta.Scope, cs *meta.ClassParseState, c *ir
 		}
 
 		if override.OverrideType == meta.OverrideNullableClassType {
-			return types.NewMap(className + "|null"), true
+			typ = types.NewMap(className + "|null")
+		} else {
+			typ = types.NewMap(className)
 		}
 
-		return types.NewMap(className), true
+	default:
+		log.Printf("Internal error: unexpected override type %d for function %s", override.OverrideType, nm)
+	}
+
+	switch override.Properties {
+	case meta.NotNull:
+		typ = typ.Filter(func(s string) bool {
+			return s != "null"
+		})
+	case meta.NotFalse:
+		typ = typ.Filter(func(s string) bool {
+			return s != "false"
+		})
+	case meta.ArrayOf:
+		typ = typ.Map(types.WrapArrayOf)
 	}
 
-	log.Printf("Internal error: unexpected override type %d for function %s", override.OverrideType, nm)
-	return types.Map{}, false
+	return typ, !typ.IsEmpty()
 }
 
 func arrayType(sc *meta.Scope, cs *meta.ClassParseState, items []*ir.ArrayItemExpr) types.Map {
diff --git a/src/types/typesmap.go b/src/types/typesmap.go
index c9e2a8d..649e976 100644
--- a/src/types/typesmap.go
+++ b/src/types/typesmap.go
@@ -81,6 +81,18 @@ func (m Map) Map(fn func(string) string) Map {
 	return NewMapFromMap(mapped)
 }
 
+// Filter returns a new types map with the types of m for which fn returns true.
+// The result type map is never marked as precise.
+func (m Map) Filter(fn func(string) bool) Map {
+	filtered := make(map[string]struct{}, len(m.m))
+	for typ := range m.m {
+		if fn(typ) {
+			filtered[typ] = struct{}{}
+		}
+	}
+	return NewMapFromMap(filtered)
+}
+
 // NewEmptyMap creates new type map that has no types in it
 func NewEmptyMap(cap int) Map {
 	return Map{m: make(map[string]struct{}, cap)}


Test patch:
diff --git a/src/linter/cache_test.go b/src/linter/cache_test.go
index d2cdc6a..ed41c9e 100644
--- a/src/linter/cache_test.go
+++ b/src/linter/cache_test.go
@@ -141,7 +141,7 @@ main();
 		//
 		// If cache encoding changes, there is a very high chance that
 		// encoded data lengh will change as well.
-		wantLen := 5316
+		wantLen := 5331
 		haveLen := buf.Len()
 		if haveLen != wantLen {
 			t.Errorf("cache len mismatch:\nhave: %d\nwant: %d", haveLen, wantLen)
@@ -150,7 +150,7 @@ main();
 		// 2. Check cache "strings" hash.
 		//
 		// It catches new fields in cached types, field renames and encoding of additional named attributes.
-		wantStrings := "63e0138101dfef86e5fa6c4ba977e34846d92999f6f4248050f3f84bca820970dcbd447a0f044ede1f32ef1f4a04b4acb5c2fb4f13caf50de6220c05e7c1e1ef"
+		wantStrings := "f14ca782fcfec5965d9d51c1aa7c6007f37304baa8848d199a36eb0d74423b2effd06dfe3af8efd9b3ab97b04e7f13acacbe6237b0dddf718287df73dc223135"
 		haveStrings := collectCacheStrings(buf.String())
 		if haveStrings != wantStrings {
 			t.Errorf("cache strings mismatch:\nhave: %q\nwant: %q", haveStrings, wantStrings)
diff --git a/src/tests/exprtype/kphp_test.go b/src/tests/exprtype/kphp_test.go
index 570121d..39ec4a1 100644
--- a/src/tests/exprtype/kphp_test.go
+++ b/src/tests/exprtype/kphp_test.go
@@ -262,6 +262,115 @@ function f() {
 	runKPHPExprTypeTest(t, &exprTypeTestParams{code: code, stubs: "<?php /* no code */"})
 }
 
+func TestNotNull(t *testing.T) {
+	code := `<?php
+class Foo {}
+
+/**
+ * @return int|null
+ */
+function f1(): int {
+	return 0;
+}
+
+function f() {
+	$a = new Foo;
+	if (1) {
+		$a = null;
+	}
+
+	exprtype(not_null($a), "\Foo");
+
+	$b = 100;
+	exprtype(not_null($b), "int");
+
+	$c = [1,2,3];
+	exprtype(not_null($c), "int[]");
+
+	$d = [1,2,3];
+	if (1) {
+		$d = null;
+	}
+	exprtype(not_null($d), "int[]");
+
+	$e = f1();
+	exprtype(not_null($e), "int|null"); // not work properly with function call type
+}
+`
+	runKPHPExprTypeTest(t, &exprTypeTestParams{code: code, stubs: "<?php /* no code */"})
+}
+
+func TestNotFalse(t *testing.T) {
+	code := `<?php
+class Foo {}
+
+/**
+ * @return int|false
+ */
+function f1(): int {
+	return 0;
+}
+
+function f() {
+	$a = new Foo;
+	if (1) {
+		$a = false;
+	}
+
+	exprtype(not_false($a), "\Foo|bool");
+
+	$b = 100;
+	exprtype(not_false($b), "int");
+
+	$c = [1,2,3];
+	exprtype(not_false($c), "int[]");
+
+	$d = [1,2,3];
+	if (1) {
+		$d = false;
+	}
+	exprtype(not_false($d), "bool|int[]");
+
+	$e = f1();
+	exprtype(not_false($e), "false|int"); // not work properly with function call type
+}
+`
+	runKPHPExprTypeTest(t, &exprTypeTestParams{code: code, stubs: "<?php /* no code */"})
+}
+
+func TestCreateVector(t *testing.T) {
+	code := `<?php
+class Foo {}
+
+/**
+ * @return int
+ */ 
+function retInt() { return 0; }
+
+/**
+ * @return int|string
+ */ 
+function retIntAndString() { return 0; }
+
+/**
+ * @return Foo
+ */ 
+function retFoo() { return 0; }
+
+function f() {
+	exprtype(create_vector(10, new Foo), "\Foo[]");
+	exprtype(create_vector(10, 1), "int[]");
+	exprtype(create_vector(10, 1.5), "float[]");
+	exprtype(create_vector(10, "22"), "string[]");
+	exprtype(create_vector(10, true ? 1 : "22"), "int[]|string[]");
+	exprtype(create_vector(10, retInt()), "int[]");
+	exprtype(create_vector(10, retIntAndString()), "int[]|string[]");
+	exprtype(create_vector(10, retFoo()), "\Foo[]");
+}
+`
+	runKPHPExprTypeTest(t, &exprTypeTestParams{code: code, stubs: "<?php /* no code */"})
+}
+
 func runKPHPExprTypeTest(t *testing.T, params *exprTypeTestParams) {
 	exprTypeTestImpl(t, params, true)
 }