Repository: VKCOM/noverify
Pull request #539 (2020-07-02 15:28:02)

phpdoc: support parsing of typed callable types
Refs #537

I also fixed the precedence issue so the `?Foo<B>` is parsed as a nullable generic type.

Signed-off-by: Iskander Sharipov <quasilyte@gmail.com>


Patch:
diff --git a/src/phpdoc/exprkind_string.go b/src/phpdoc/exprkind_string.go
index 7251992..beb94e5 100644
--- a/src/phpdoc/exprkind_string.go
+++ b/src/phpdoc/exprkind_string.go
@@ -22,11 +22,12 @@ func _() {
 	_ = x[ExprUnion-11]
 	_ = x[ExprInter-12]
 	_ = x[ExprGeneric-13]
+	_ = x[ExprTypedCallable-14]
 }
 
-const _ExprKind_name = "InvalidUnknownNameSpecialNameIntKeyValArrayParenNullableOptionalNotUnionInterGeneric"
+const _ExprKind_name = "InvalidUnknownNameSpecialNameIntKeyValArrayParenNullableOptionalNotUnionInterGenericTypedCallable"
 
-var _ExprKind_index = [...]uint8{0, 7, 14, 18, 29, 32, 38, 43, 48, 56, 64, 67, 72, 77, 84}
+var _ExprKind_index = [...]uint8{0, 7, 14, 18, 29, 32, 38, 43, 48, 56, 64, 67, 72, 77, 84, 97}
 
 func (i ExprKind) String() string {
 	if i >= ExprKind(len(_ExprKind_index)-1) {
diff --git a/src/phpdoc/type_parser.go b/src/phpdoc/type_parser.go
index bae289c..f6e2970 100644
--- a/src/phpdoc/type_parser.go
+++ b/src/phpdoc/type_parser.go
@@ -59,8 +59,8 @@ const (
 
 	// ExprKeyVal is `key:val` type.
 	// Examples: `name: string` `id:int`
-	// Args[0]: key expression (left)
-	// Args[1]: val expression (right)
+	// Args[0] - key expression (left)
+	// Args[1] - val expression (right)
 	ExprKeyVal
 
 	// ExprArray is `elem[]` or `[]elem` type.
@@ -113,6 +113,12 @@ const (
 	//
 	// Note: may miss closing `>`.
 	ExprGeneric
+
+	// ExprTypedCallable is a parametrized `callable(A,...):B` type.
+	// Examples: `callable():void` `callable(int, int) : float`
+	// Args[0] - return type
+	// Args[1:] - argument types
+	ExprTypedCallable
 )
 
 const (
@@ -133,9 +139,9 @@ var infixPrecedenceTab = [256]byte{
 	'|': 2,
 	'&': 3,
 	'[': 4,
-	'<': 4,
-	'(': 4,
-	'{': 4,
+	'<': 5,
+	'(': 5,
+	'{': 5,
 	'?': 4,
 }
 
@@ -170,11 +176,15 @@ func (p *TypeParser) reset(input string) {
 	p.skipUnknown = false
 }
 
+func (p *TypeParser) exprValue(e *TypeExpr) string {
+	return p.input[e.Begin:e.End]
+}
+
 func (p *TypeParser) setValues(e *TypeExpr) {
 	for i := range e.Args {
 		p.setValues(&e.Args[i])
 	}
-	e.Value = p.input[e.Begin:e.End]
+	e.Value = p.exprValue(e)
 }
 
 func (p *TypeParser) parseExprInsideGroup() *TypeExpr {
@@ -342,6 +352,21 @@ func (p *TypeParser) parseExpr(precedence byte) *TypeExpr {
 					p.pos++
 				}
 			}
+			// For `callable(...)` case we want to see whether we can peek ':'.
+			// If we can, parse it as a typed callable.
+			if shape == ShapeGenericParen && p.exprValue(&left.Args[0]) == "callable" {
+				pos := p.pos
+				p.skipWhitespace()
+				if p.peek() == ':' {
+					p.pos++
+					returnType := p.parseExprInsideGroup()
+					left.Args[0] = *returnType
+					left.Kind = ExprTypedCallable
+					left.Shape = ShapeDefault
+				} else {
+					p.pos = pos // Unread whitespace
+				}
+			}
 			left.End = uint16(p.pos)
 		}
 	}


Test patch:
diff --git a/src/phpdoc/type_parser_test.go b/src/phpdoc/type_parser_test.go
index d5b591d..b633de5 100644
--- a/src/phpdoc/type_parser_test.go
+++ b/src/phpdoc/type_parser_test.go
@@ -75,6 +75,7 @@ func TestParser(t *testing.T) {
 		{`array<int>`, `Generic="array<int>"{Name="array" Name="int"}`},
 		{`array<int,string>`, `Generic="array<int,string>"{Name="array" Name="int" Name="string"}`},
 		{`array<int, array<string, stdclass> >`, `Generic="array<int, array<string, stdclass> >"{Name="array" Name="int" Generic="array<string, stdclass>"{Name="array" Name="string" Name="stdclass"}}`},
+		{`?A<B>`, `Nullable="?A<B>"{Generic="A<B>"{Name="A" Name="B"}}`},
 
 		// Alternative generic syntax 1.
 		{`tuple(*)`, `GenericParen="tuple(*)"{Name="tuple" SpecialName="*"}`},
@@ -87,11 +88,26 @@ func TestParser(t *testing.T) {
 		{`array{a: int, b: float}`, `GenericBrace="array{a: int, b: float}"{Name="array" KeyVal="a: int"{Name="a" Name="int"} KeyVal="b: float"{Name="b" Name="float"}}`},
 		{`array{a : int}`, `GenericBrace="array{a : int}"{Name="array" KeyVal="a : int"{Name="a" Name="int"}}`},
 
+		// Typed callable (see #537).
+		// TODO: add variadic params support.
+		{`callable() : void`, `TypedCallable="callable() : void"{Name="void"}`},
+		{`callable(A):B`, `TypedCallable="callable(A):B"{Name="B" Name="A"}`},
+		{`(callable (A, B) : C)`, `Paren="(callable (A, B) : C)"{TypedCallable="callable (A, B) : C"{Name="C" Name="A" Name="B"}}`},
+		{`?callable() : int`, `Nullable="?callable() : int"{TypedCallable="callable() : int"{Name="int"}}`},
+		{`callable(A) : callable(B) : int`, `TypedCallable="callable(A) : callable(B) : int"{TypedCallable="callable(B) : int"{Name="int" Name="B"} Name="A"}`},
+
+		// The typed callable without return type is parsed as generic type.
+		// Only () shape is recognized.
+		{`callable(A, B)`, `GenericParen="callable(A, B)"{Name="callable" Name="A" Name="B"}`},
+		{`callable()`, `GenericParen="callable()"{Name="callable"}`},
+		{`callable<A>:B`, `KeyVal="callable<A>:B"{Generic="callable<A>"{Name="callable" Name="A"} Name="B"}`},
+
 		// KeyVal types.
 		{`name:int`, `KeyVal="name:int"{Name="name" Name="int"}`},
 		{`array{foo: int}`, `GenericBrace="array{foo: int}"{Name="array" KeyVal="foo: int"{Name="foo" Name="int"}}`},
 		{`array{0: int}`, `GenericBrace="array{0: int}"{Name="array" KeyVal="0: int"{Int="0" Name="int"}}`},
 		{`shape{s?: string}`, `GenericBrace="shape{s?: string}"{Name="shape" KeyVal="s?: string"{Optional="s?"{Name="s"} Name="string"}}`},
+		{`foo(A):B`, `KeyVal="foo(A):B"{GenericParen="foo(A)"{Name="foo" Name="A"} Name="B"}`},
 
 		// Intersection types has higher priority that union types.
 		{`x&y|z`, `Union="x&y|z"{Inter="x&y"{Name="x" Name="y"} Name="z"}`},