Repository: 99designs/gqlgen (go)

Issue:
Method name collision on model results in buggy resolver
### What happened?

When a model has a method named after a field it should resolve but has the incorrect signature, and the field has parameters, the resulting generated resolver has the incorrect signature. In particular the input parameters are missing.

### What did you expect?

The input parameters to be part of the generated resolver signature. If not possible then second best option is for the generation to error.

### Minimal graphql.schema and models to reproduce

Schema:
```
type Todo {
  working(in: Int): Int
  broken(in: Int): Int
}

type Query {
  todo: Todo
}
```

Model:
```
package model

import "context"

type Todo struct{}

func (*Todo) Broken(ctx context.Context) string {
	return ""
}
```

Resulting generated resolvers:
```
func (r *todoResolver) Working(ctx context.Context, obj *model.Todo, in *int) (*int, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *todoResolver) Broken(ctx context.Context, obj *model.Todo) (*int, error) {
	panic(fmt.Errorf("not implemented"))
}
```

Expected generated resolver:
```
func (r *todoResolver) Working(ctx context.Context, obj *model.Todo, in *int) (*int, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *todoResolver) Broken(ctx context.Context, obj *model.Todo, in *int) (*int, error) {
	panic(fmt.Errorf("not implemented"))
}
```

### versions
 - `gqlgen version` - `v0.12.2`
 - `go version` - `1.13`
 - dep or go modules - `modules`

### extra

You can find all the code related to this issue as generated by walking through the getting started guide  and adapted accordingly in [this repo](https://github.com/matiasanaya/gqlgen-scratch/tree/main/shadow-resolver).

Pull request:
Don't overwrite field arguments when none match
Problem: during GraphQL field binding, when there's matching Golang method, yet without any arguments besides `ctx`, a resolver with no arguments from the original GraphQL schema will be generated.

Solution: don't overwrite field's arguments until we're sure we've got a perfect match.

Resolves #1319.

Patch:
diff --git a/codegen/args.go b/codegen/args.go
index 9a9c8fdb..8c08d924 100644
--- a/codegen/args.go
+++ b/codegen/args.go
@@ -73,7 +73,7 @@ func (b *builder) buildArg(obj *Object, arg *ast.ArgumentDefinition) (*FieldArgu
 	return &newArg, nil
 }
 
-func (b *builder) bindArgs(field *Field, params *types.Tuple) error {
+func (b *builder) bindArgs(field *Field, params *types.Tuple) ([]*FieldArgument, error) {
 	var newArgs []*FieldArgument
 
 nextArg:
@@ -83,7 +83,7 @@ nextArg:
 			if strings.EqualFold(oldArg.Name, param.Name()) {
 				tr, err := b.Binder.TypeReference(oldArg.Type, param.Type())
 				if err != nil {
-					return err
+					return nil, err
 				}
 				oldArg.TypeReference = tr
 
@@ -93,11 +93,10 @@ nextArg:
 		}
 
 		// no matching arg found, abort
-		return fmt.Errorf("arg %s not in schema", param.Name())
+		return nil, fmt.Errorf("arg %s not in schema", param.Name())
 	}
 
-	field.Args = newArgs
-	return nil
+	return newArgs, nil
 }
 
 func (a *Data) Args() map[string][]*FieldArgument {
diff --git a/codegen/field.go b/codegen/field.go
index 7311b42c..867215e5 100644
--- a/codegen/field.go
+++ b/codegen/field.go
@@ -179,10 +179,13 @@ func (b *builder) bindField(obj *Object, f *Field) (errret error) {
 			params = types.NewTuple(vars...)
 		}
 
-		if err = b.bindArgs(f, params); err != nil {
+		// Try to match target function's arguments with GraphQL field arguments
+		newArgs, err := b.bindArgs(f, params)
+		if err != nil {
 			return fmt.Errorf("%s:%d: %w", pos.Filename, pos.Line, err)
 		}
 
+		// Try to match target function's return types with GraphQL field return type
 		result := sig.Results().At(0)
 		tr, err := b.Binder.TypeReference(f.Type, result.Type())
 		if err != nil {
@@ -193,6 +196,7 @@ func (b *builder) bindField(obj *Object, f *Field) (errret error) {
 		f.GoFieldType = GoFieldMethod
 		f.GoReceiverName = "obj"
 		f.GoFieldName = target.Name()
+		f.Args = newArgs
 		f.TypeReference = tr
 
 		return nil
diff --git a/example/chat/readme.md b/example/chat/readme.md
index 416a9fb0..1cb6e733 100644
--- a/example/chat/readme.md
+++ b/example/chat/readme.md
@@ -3,26 +3,32 @@
 Example app using subscriptions to build a chat room.
 
 ### Server
+
 ```bash
 go run ./server/server.go
 ```
 
 ### Client
+
 The react app uses two different implementation for the websocket link
+
 - [apollo-link-ws](https://www.apollographql.com/docs/react/api/link/apollo-link-ws) which uses the deprecated [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) library
 - [graphql-ws](https://github.com/enisdenjo/graphql-ws)
 
 First you need to install the dependencies
+
 ```bash
-npm install 
+npm install
 ```
 
 Then to run the app with the `apollo-link-ws` implementation do
+
 ```bash
 npm run start
 ```
 
 or to run the app with the `graphql-ws` implementation (and the newer `graphql-transport-ws` protocol) do
+
 ```bash
 npm run start:graphql-transport-ws
 ```
diff --git a/graphql/handler/transport/websocket.go b/graphql/handler/transport/websocket.go
index bb148aec..d6dada03 100644
--- a/graphql/handler/transport/websocket.go
+++ b/graphql/handler/transport/websocket.go
@@ -22,6 +22,7 @@ type (
 		Upgrader              websocket.Upgrader
 		InitFunc              WebsocketInitFunc
 		KeepAlivePingInterval time.Duration
+		PingPongInterval      time.Duration
 
 		didInjectSubprotocols bool
 	}
@@ -33,6 +34,7 @@ type (
 		active          map[string]context.CancelFunc
 		mu              sync.Mutex
 		keepAliveTicker *time.Ticker
+		pingPongTicker  *time.Ticker
 		exec            graphql.GraphExecutor
 
 		initPayload InitPayload
@@ -157,6 +159,17 @@ func (c *wsConnection) run() {
 		go c.keepAlive(ctx)
 	}
 
+	// Create a timer that will fire every interval a ping message that should
+	// receive a pong (SetPongHandler in init() function)
+	if c.PingPongInterval != 0 {
+		c.mu.Lock()
+		c.pingPongTicker = time.NewTicker(c.PingPongInterval)
+		c.mu.Unlock()
+
+		c.conn.SetReadDeadline(time.Now().UTC().Add(2 * c.PingPongInterval))
+		go c.ping(ctx)
+	}
+
 	for {
 		start := graphql.Now()
 		m, err := c.me.NextMessage()
@@ -178,6 +191,10 @@ func (c *wsConnection) run() {
 		case connectionCloseMessageType:
 			c.close(websocket.CloseNormalClosure, "terminated")
 			return
+		case pingMesageType:
+			c.write(&message{t: pongMessageType, payload: m.payload})
+		case pongMessageType:
+			c.conn.SetReadDeadline(time.Now().UTC().Add(2 * c.PingPongInterval))
 		default:
 			c.sendConnectionError("unexpected message %s", m.t)
 			c.close(websocket.CloseProtocolError, "unexpected message")
@@ -198,6 +215,18 @@ func (c *wsConnection) keepAlive(ctx context.Context) {
 	}
 }
 
+func (c *wsConnection) ping(ctx context.Context) {
+	for {
+		select {
+		case <-ctx.Done():
+			c.pingPongTicker.Stop()
+			return
+		case <-c.pingPongTicker.C:
+			c.write(&message{t: pingMesageType, payload: json.RawMessage{}})
+		}
+	}
+}
+
 func (c *wsConnection) subscribe(start time.Time, msg *message) {
 	ctx := graphql.StartOperationTrace(c.ctx)
 	var params *graphql.RawParams
@@ -315,6 +344,9 @@ func (c *wsConnection) sendConnectionError(format string, args ...interface{}) {
 func (c *wsConnection) close(closeCode int, message string) {
 	c.mu.Lock()
 	_ = c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(closeCode, message))
+	for _, closer := range c.active {
+		closer()
+	}
 	c.mu.Unlock()
 	_ = c.conn.Close()
 }
diff --git a/graphql/handler/transport/websocket_graphql_transport_ws.go b/graphql/handler/transport/websocket_graphql_transport_ws.go
index 35c6524b..de044042 100644
--- a/graphql/handler/transport/websocket_graphql_transport_ws.go
+++ b/graphql/handler/transport/websocket_graphql_transport_ws.go
@@ -17,16 +17,22 @@ const (
 	graphqltransportwsNextMsg           = graphqltransportwsMessageType("next")
 	graphqltransportwsErrorMsg          = graphqltransportwsMessageType("error")
 	graphqltransportwsCompleteMsg       = graphqltransportwsMessageType("complete")
+	graphqltransportwsPingMsg           = graphqltransportwsMessageType("ping")
+	graphqltransportwsPongMsg           = graphqltransportwsMessageType("pong")
 )
 
-var allGraphqltransportwsMessageTypes = []graphqltransportwsMessageType{
-	graphqltransportwsConnectionInitMsg,
-	graphqltransportwsConnectionAckMsg,
-	graphqltransportwsSubscribeMsg,
-	graphqltransportwsNextMsg,
-	graphqltransportwsErrorMsg,
-	graphqltransportwsCompleteMsg,
-}
+var (
+	allGraphqltransportwsMessageTypes = []graphqltransportwsMessageType{
+		graphqltransportwsConnectionInitMsg,
+		graphqltransportwsConnectionAckMsg,
+		graphqltransportwsSubscribeMsg,
+		graphqltransportwsNextMsg,
+		graphqltransportwsErrorMsg,
+		graphqltransportwsCompleteMsg,
+		graphqltransportwsPingMsg,
+		graphqltransportwsPongMsg,
+	}
+)
 
 type (
 	graphqltransportwsMessageExchanger struct {
@@ -103,6 +109,10 @@ func (m graphqltransportwsMessage) toMessage() (message, error) {
 		t = startMessageType
 	case graphqltransportwsCompleteMsg:
 		t = stopMessageType
+	case graphqltransportwsPingMsg:
+		t = pingMesageType
+	case graphqltransportwsPongMsg:
+		t = pongMessageType
 	}
 
 	return message{
@@ -131,6 +141,10 @@ func (m *graphqltransportwsMessage) fromMessage(msg *message) (err error) {
 		m.Type = graphqltransportwsCompleteMsg
 	case errorMessageType:
 		m.Type = graphqltransportwsErrorMsg
+	case pingMesageType:
+		m.Type = graphqltransportwsPingMsg
+	case pongMessageType:
+		m.Type = graphqltransportwsPongMsg
 	}
 
 	return err
diff --git a/graphql/handler/transport/websocket_subprotocol.go b/graphql/handler/transport/websocket_subprotocol.go
index f47bfee8..83639e3c 100644
--- a/graphql/handler/transport/websocket_subprotocol.go
+++ b/graphql/handler/transport/websocket_subprotocol.go
@@ -18,6 +18,8 @@ const (
 	dataMessageType
 	completeMessageType
 	errorMessageType
+	pingMesageType
+	pongMessageType
 )
 
 var (
@@ -68,6 +70,10 @@ func (t messageType) String() string {
 		text = "complete"
 	case errorMessageType:
 		text = "error"
+	case pingMesageType:
+		text = "ping"
+	case pongMessageType:
+		text = "pong"
 	}
 	return text
 }
diff --git a/handler/handler.go b/handler/handler.go
index ef493279..386b1946 100644
--- a/handler/handler.go
+++ b/handler/handler.go
@@ -29,6 +29,7 @@ func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc
 		Upgrader:              cfg.upgrader,
 		InitFunc:              cfg.websocketInitFunc,
 		KeepAlivePingInterval: cfg.connectionKeepAlivePingInterval,
+		PingPongInterval:      cfg.connectionPingPongInterval,
 	})
 	srv.AddTransport(transport.Options{})
 	srv.AddTransport(transport.GET{})
@@ -77,6 +78,7 @@ type Config struct {
 	upgrader                        websocket.Upgrader
 	websocketInitFunc               transport.WebsocketInitFunc
 	connectionKeepAlivePingInterval time.Duration
+	connectionPingPongInterval      time.Duration
 	recover                         graphql.RecoverFunc
 	errorPresenter                  graphql.ErrorPresenterFunc
 	fieldHooks                      []graphql.FieldMiddleware
@@ -210,6 +212,12 @@ func WebsocketKeepAliveDuration(duration time.Duration) Option {
 	}
 }
 
+func WebsocketPingPongDuration(duration time.Duration) Option {
+	return func(cfg *Config) {
+		cfg.connectionPingPongInterval = duration
+	}
+}
+
 // Add cache that will hold queries for automatic persisted queries (APQ)
 // Deprecated: switch to graphql/handler.New
 func EnablePersistedQueryCache(cache PersistedQueryCache) Option {


Test patch:
diff --git a/graphql/handler/transport/websocket_test.go b/graphql/handler/transport/websocket_test.go
index e5bf8478..af82c616 100644
--- a/graphql/handler/transport/websocket_test.go
+++ b/graphql/handler/transport/websocket_test.go
@@ -316,6 +316,39 @@ func TestWebsocketGraphqltransportwsSubprotocol(t *testing.T) {
 	})
 }
 
+func TestWebsocketWithPingPongInterval(t *testing.T) {
+	handler := testserver.New()
+	handler.AddTransport(transport.Websocket{
+		PingPongInterval: time.Second * 1,
+	})
+
+	srv := httptest.NewServer(handler)
+	defer srv.Close()
+
+	t.Run("client receives ping and responds with pong", func(t *testing.T) {
+		c := wsConnectWithSubprocotol(srv.URL, graphqltransportwsSubprotocol)
+		defer c.Close()
+
+		require.NoError(t, c.WriteJSON(&operationMessage{Type: graphqltransportwsConnectionInitMsg}))
+		assert.Equal(t, graphqltransportwsConnectionAckMsg, readOp(c).Type)
+
+		assert.Equal(t, graphqltransportwsPingMsg, readOp(c).Type)
+		require.NoError(t, c.WriteJSON(&operationMessage{Type: graphqltransportwsPongMsg}))
+		assert.Equal(t, graphqltransportwsPingMsg, readOp(c).Type)
+	})
+
+	t.Run("client sends ping and expects pong", func(t *testing.T) {
+		c := wsConnectWithSubprocotol(srv.URL, graphqltransportwsSubprotocol)
+		defer c.Close()
+
+		require.NoError(t, c.WriteJSON(&operationMessage{Type: graphqltransportwsConnectionInitMsg}))
+		assert.Equal(t, graphqltransportwsConnectionAckMsg, readOp(c).Type)
+
+		require.NoError(t, c.WriteJSON(&operationMessage{Type: graphqltransportwsPingMsg}))
+		assert.Equal(t, graphqltransportwsPongMsg, readOp(c).Type)
+	})
+}
+
 func wsConnect(url string) *websocket.Conn {
 	return wsConnectWithSubprocotol(url, "")
 }
@@ -374,6 +407,8 @@ const (
 	graphqltransportwsSubscribeMsg      = "subscribe"
 	graphqltransportwsNextMsg           = "next"
 	graphqltransportwsCompleteMsg       = "complete"
+	graphqltransportwsPingMsg           = "ping"
+	graphqltransportwsPongMsg           = "pong"
 )
 
 type operationMessage struct {