Repository: VKCOM/noverify
Pull request #755 (2020-09-03 06:48:43)

linter,rules: move type matching code to rules
This code is used in rules-related linter branch,
so it makes sense to move it to the rules package.

Signed-off-by: Iskander Sharipov <quasilyte@gmail.com>


Patch:
diff --git a/src/linter/root.go b/src/linter/root.go
index ec3e883..ccc8156 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -1846,7 +1846,7 @@ func (d *RootWalker) checkTypeFilter(wantType *phpdoc.Type, sc *meta.Scope, nn i
 	// Can we use `meta.Type` for this?
 	typ := solver.ExprType(sc, d.ctx.st, nn)
 	haveType := typesMapToTypeExpr(d.ctx.phpdocTypeParser, typ)
-	return typeIsCompatible(wantType.Expr, haveType.Expr)
+	return rules.TypeIsCompatible(wantType.Expr, haveType.Expr)
 }
 
 func (d *RootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filterSet map[string]rules.Filter) bool {
diff --git a/src/linter/utils.go b/src/linter/utils.go
index 8313ee1..61216e0 100644
--- a/src/linter/utils.go
+++ b/src/linter/utils.go
@@ -89,55 +89,6 @@ func typesMapToTypeExpr(p *phpdoc.TypeParser, m meta.TypesMap) phpdoc.Type {
 	return p.Parse(typeString)
 }
 
-// typesIsCompatible reports whether val type is compatible with dst type.
-func typeIsCompatible(dst, val phpdoc.TypeExpr) bool {
-	// TODO: allow implementations to be compatible with interfaces.
-	// TODO: allow derived classes to be compatible with base classes.
-
-	for val.Kind == phpdoc.ExprParen {
-		val = val.Args[0]
-	}
-
-	switch dst.Kind {
-	case phpdoc.ExprParen:
-		return typeIsCompatible(dst.Args[0], val)
-
-	case phpdoc.ExprName:
-		switch dst.Value {
-		case "object":
-			// For object we accept any kind of object instance.
-			// https://wiki.php.net/rfc/object-typehint
-			return val.Kind == dst.Kind && (val.Value == "object" || strings.HasPrefix(val.Value, `\`))
-		case "array":
-			return val.Kind == phpdoc.ExprArray
-		}
-		return val.Kind == dst.Kind && dst.Value == val.Value
-
-	case phpdoc.ExprNot:
-		return !typeIsCompatible(dst.Args[0], val)
-
-	case phpdoc.ExprNullable:
-		return val.Kind == dst.Kind && typeIsCompatible(dst.Args[0], val.Args[0])
-
-	case phpdoc.ExprArray:
-		return val.Kind == dst.Kind && typeIsCompatible(dst.Args[0], val.Args[0])
-
-	case phpdoc.ExprUnion:
-		if val.Kind == dst.Kind {
-			return typeIsCompatible(dst.Args[0], val.Args[0]) &&
-				typeIsCompatible(dst.Args[1], val.Args[1])
-		}
-		return typeIsCompatible(dst.Args[0], val) || typeIsCompatible(dst.Args[1], val)
-
-	case phpdoc.ExprInter:
-		// TODO: make it work as intended. (See #310)
-		return false
-
-	default:
-		return false
-	}
-}
-
 type funcCallInfo struct {
 	canAnalyze bool
 	defined    bool
diff --git a/src/rules/typematch.go b/src/rules/typematch.go
new file mode 100644
index 0000000..3578fc3
--- /dev/null
+++ b/src/rules/typematch.go
@@ -0,0 +1,67 @@
+package rules
+
+import (
+	"strings"
+
+	"github.com/VKCOM/noverify/src/phpdoc"
+)
+
+// TypesIsCompatible reports whether val type is compatible with dst type.
+func TypeIsCompatible(dst, val phpdoc.TypeExpr) bool {
+	// TODO: allow implementations to be compatible with interfaces.
+	// TODO: allow derived classes to be compatible with base classes.
+
+	for val.Kind == phpdoc.ExprParen {
+		val = val.Args[0]
+	}
+
+	switch dst.Kind {
+	case phpdoc.ExprParen:
+		return TypeIsCompatible(dst.Args[0], val)
+
+	case phpdoc.ExprName:
+		switch dst.Value {
+		case "object":
+			// For object we accept any kind of object instance.
+			// https://wiki.php.net/rfc/object-typehint
+			return val.Kind == dst.Kind && (val.Value == "object" || strings.HasPrefix(val.Value, `\`))
+		case "array":
+			return val.Kind == phpdoc.ExprArray
+		}
+		return val.Kind == dst.Kind && dst.Value == val.Value
+
+	case phpdoc.ExprNot:
+		return !TypeIsCompatible(dst.Args[0], val)
+
+	case phpdoc.ExprNullable:
+		return val.Kind == dst.Kind && TypeIsCompatible(dst.Args[0], val.Args[0])
+
+	case phpdoc.ExprArray:
+		return val.Kind == dst.Kind && TypeIsCompatible(dst.Args[0], val.Args[0])
+
+	case phpdoc.ExprUnion:
+		// TODO: sort the union types and avoid O(n^2) in the worst case?
+		if val.Kind == phpdoc.ExprUnion {
+			// Two union types are compatible if all their variants are compatible.
+			for _, variant := range val.Args {
+				if !TypeIsCompatible(dst, variant) {
+					return false
+				}
+			}
+			return true
+		}
+		for _, variant := range dst.Args {
+			if TypeIsCompatible(variant, val) {
+				return true
+			}
+		}
+		return false
+
+	case phpdoc.ExprInter:
+		// TODO: make it work as intended. (See #310)
+		return false
+
+	default:
+		return false
+	}
+}


Test patch:
diff --git a/src/linter/phpdoc_test.go b/src/linter/phpdoc_test.go
index d529eb2..864a250 100644
--- a/src/linter/phpdoc_test.go
+++ b/src/linter/phpdoc_test.go
@@ -5,7 +5,6 @@ import (
 	"testing"
 
 	"github.com/VKCOM/noverify/src/meta"
-	"github.com/VKCOM/noverify/src/phpdoc"
 )
 
 func TestParseClassPHPDoc(t *testing.T) {
@@ -108,104 +107,6 @@ func TestParseClassPHPDoc(t *testing.T) {
 	}
 }
 
-func TestTypeFilter(t *testing.T) {
-	type testCase struct {
-		dst string
-		val string
-	}
-
-	matchingTests := []testCase{
-		{`array`, `mixed[]`},
-		{`array`, `int[]`},
-		{`array`, `\Foo[]`},
-
-		{`object`, `object`},
-		{`object`, `\Foo`},
-		{`object`, `\Foo\Bar`},
-
-		{`!int`, `string`},
-		{`!int`, `mixed`},
-		{`!array`, `int`},
-		{`!array`, `string`},
-
-		{`int[]`, `int[]`},
-
-		{`int`, `(int)`},
-		{`(int)`, `int`},
-		{`(int)`, `((int))`},
-
-		{`int|float`, `int`},
-		{`int|float`, `float`},
-		{`float|int`, `int`},
-		{`int|float`, `float`},
-
-		{`!(int|float)`, `string`},
-		{`!(int|float)`, `int[]`},
-		{`!(object|array)`, `int`},
-		{`!(object|array)`, `string`},
-
-		{`a|b`, `a|b`},
-		{`object|array`, `\Foo|int[]`},
-		{`object|array`, `object|float[]`},
-
-		// TODO: make union comparison work in these cases.
-		// {`a|b|c`, `a|b|c`},
-		// {`a|c|b`, `a|b|c`},
-		// {`c|b|a`, `a|b|c`},
-	}
-
-	nonMatchingTests := []testCase{
-		{`array`, `int`},
-		{`array`, `mixed`},
-		{`array`, `\Foo`},
-
-		{`object`, `int`},
-		{`object`, `\Foo[]`},
-		{`object`, `mixed`},
-
-		{`!int`, `int`},
-		{`!array`, `mixed[]`},
-		{`!array`, `int[]`},
-
-		{`int[]`, `float[]`},
-		{`int[]`, `mixed[]`},
-
-		{`int|float`, `string`},
-		{`int|float`, `\Foo`},
-		{`float|int`, `int[]`},
-		{`int|float`, `float[]`},
-		{`int|float`, `mixed`},
-
-		{`!(int|float)`, `int`},
-		{`!(int|float)`, `float`},
-		{`!(object|array)`, `object`},
-		{`!(object|array)`, `int[]`},
-		{`!(object|array)`, `\Foo`},
-		{`!(object|array)`, `\Foo[]`},
-
-		{`object|array`, `int|int[]`},
-		{`object|array`, `object|float`},
-		{`object|array`, `string|float`},
-	}
-
-	runTests := func(want bool, tests []testCase) {
-		p := phpdoc.NewTypeParser()
-		for _, test := range tests {
-			val := p.Parse(test.val).Clone()
-			dst := p.Parse(test.dst).Clone()
-
-			have := typeIsCompatible(dst.Expr, val.Expr)
-			if have != want {
-				t.Errorf("incorrect result: compatible(%s, %s) => %v",
-					test.dst, test.val, have)
-			}
-		}
-	}
-
-	runTests(true, matchingTests)
-	runTests(false, nonMatchingTests)
-}
-
 func BenchmarkParseTypes(b *testing.B) {
 	st := &meta.ClassParseState{}
 	ctx := newRootContext(NewWorkerContext(), st)
diff --git a/src/rules/typematch_test.go b/src/rules/typematch_test.go
new file mode 100644
index 0000000..4fbff82
--- /dev/null
+++ b/src/rules/typematch_test.go
@@ -0,0 +1,104 @@
+package rules
+
+import (
+	"testing"
+
+	"github.com/VKCOM/noverify/src/phpdoc"
+)
+
+func TestTypeFilter(t *testing.T) {
+	type testCase struct {
+		dst string
+		val string
+	}
+
+	matchingTests := []testCase{
+		{`array`, `mixed[]`},
+		{`array`, `int[]`},
+		{`array`, `\Foo[]`},
+
+		{`object`, `object`},
+		{`object`, `\Foo`},
+		{`object`, `\Foo\Bar`},
+
+		{`!int`, `string`},
+		{`!int`, `mixed`},
+		{`!array`, `int`},
+		{`!array`, `string`},
+
+		{`int[]`, `int[]`},
+
+		{`int`, `(int)`},
+		{`(int)`, `int`},
+		{`(int)`, `((int))`},
+
+		{`int|float`, `int`},
+		{`int|float`, `float`},
+		{`float|int`, `int`},
+		{`int|float`, `float`},
+
+		{`!(int|float)`, `string`},
+		{`!(int|float)`, `int[]`},
+		{`!(object|array)`, `int`},
+		{`!(object|array)`, `string`},
+
+		{`a|b`, `a|b`},
+		{`object|array`, `\Foo|int[]`},
+		{`object|array`, `object|float[]`},
+
+		{`a|b|c`, `a|b|c`},
+		{`a|c|b`, `a|b|c`},
+		{`c|b|a`, `a|b|c`},
+	}
+
+	nonMatchingTests := []testCase{
+		{`array`, `int`},
+		{`array`, `mixed`},
+		{`array`, `\Foo`},
+
+		{`object`, `int`},
+		{`object`, `\Foo[]`},
+		{`object`, `mixed`},
+
+		{`!int`, `int`},
+		{`!array`, `mixed[]`},
+		{`!array`, `int[]`},
+
+		{`int[]`, `float[]`},
+		{`int[]`, `mixed[]`},
+
+		{`int|float`, `string`},
+		{`int|float`, `\Foo`},
+		{`float|int`, `int[]`},
+		{`int|float`, `float[]`},
+		{`int|float`, `mixed`},
+
+		{`!(int|float)`, `int`},
+		{`!(int|float)`, `float`},
+		{`!(object|array)`, `object`},
+		{`!(object|array)`, `int[]`},
+		{`!(object|array)`, `\Foo`},
+		{`!(object|array)`, `\Foo[]`},
+
+		{`object|array`, `int|int[]`},
+		{`object|array`, `object|float`},
+		{`object|array`, `string|float`},
+	}
+
+	runTests := func(want bool, tests []testCase) {
+		p := phpdoc.NewTypeParser()
+		for _, test := range tests {
+			val := p.Parse(test.val).Clone()
+			dst := p.Parse(test.dst).Clone()
+
+			have := TypeIsCompatible(dst.Expr, val.Expr)
+			if have != want {
+				t.Errorf("incorrect result: compatible(%s, %s) => %v",
+					test.dst, test.val, have)
+			}
+		}
+	}
+
+	runTests(true, matchingTests)
+	runTests(false, nonMatchingTests)
+}