Skip to content

Commit

Permalink
Merge pull request #1 from gqlgo/imp/first
Browse files Browse the repository at this point in the history
Implement nodecheck
  • Loading branch information
bannzai authored Jan 12, 2022
2 parents cd65c44 + a4e1f50 commit c129568
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 0 deletions.
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# nodecheck

[![pkg.go.dev][gopkg-badge]][gopkg]

**nodecheck** will find any GraphQL schema that is not conform to Node interface.

```graphql
# Valid
type User implements Node {
id: ID!
}

# Invalid. not conform to Node
type Community {
name: String!
}

# Invalid. not conform to Node but id is exist
type Item {
id: ID!
}
```

## How to use

A runnable linter can be created with multichecker package.
You can create own linter with your favorite Analyzers.
And nodecheck has independ flag for allow exclude types of nodecheck.

Full example

```go
func main() {
var excludes string
flag.StringVar(&excludes, "excludes", "", "exclude GraphQL types for node check. it can specify multiple values separated by `,` and it can use regex(e.g .+Connection")
flag.Parse()

analyzer := nodecheck.Analyzer(excludes)

multichecker.Main(
analyzer,
)
}
```

`nodecheck` provides a executable binary. So, you can get cmd tool via `go install` command.

```sh
$ go install github.com/gqlgo/nodecheck/cmd/nodecheck@latest
```

The `nodecheck` command receive two flags, `schema` and `excludes`. `excludes` can specify with regex format and it can receive multiple arguments separated by ','.

```sh
$ nodecheck -schema="server/graphql/schema/**/*.graphql" -excludes=.+Connection,.+Edge
```

## Author

[![Appify Technologies, Inc.](appify-logo.png)](http://github.com/appify-technologies)

<!-- links -->
[gopkg]: https://pkg.go.dev/github.com/gqlgo/nodecheck
[gopkg-badge]: https://pkg.go.dev/badge/github.com/gqlgo/nodecheck?status.svg

20 changes: 20 additions & 0 deletions cmd/nodecheck/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import (
"flag"

"github.com/bannzai/nodecheck"
"github.com/gqlgo/gqlanalysis/multichecker"
)

func main() {
var excludes string
flag.StringVar(&excludes, "excludes", "", "exclude GraphQL types for node check. it can specify multiple values separated by `,` and it can use regex(e.g .+Connection")
flag.Parse()

analyzer := nodecheck.Analyzer(excludes)

multichecker.Main(
analyzer,
)
}
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Yamashou/gqlgenc v0.0.0-20210330020310-5eab2091f840 h1:pLmcSh/7xffZydsct+KAX8nMXYL6b4eRpT5OKCGUGdA=
github.com/Yamashou/gqlgenc v0.0.0-20210330020310-5eab2091f840/go.mod h1:BivGCpycfS8C36iVex2xH2j+kJa3/ca/42kdoGO6I6o=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand All @@ -24,6 +28,7 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gqlgo/gqlanalysis v0.2.1 h1:lYfI4ez0LXj8N25q6mWew+eDsYSo4AnXZ9z7CWm6GOU=
github.com/gqlgo/gqlanalysis v0.2.1/go.mod h1:BFhVkoQezTAcK1G9e1Yety8DVKjV4Ai5reZchzq7Jl4=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a h1:8NZHLa6Gp0hW6xJ0c3F1Kse7dJw30fOcDzHuF9sLbnE=
github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a/go.mod h1:izVPOvVRsHiKkeGCT6tYBNWyDVuzj9wAaBb5R9qamfw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand All @@ -33,6 +38,7 @@ github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIG
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
Expand Down Expand Up @@ -60,7 +66,9 @@ github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7
github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
Expand Down Expand Up @@ -90,10 +98,12 @@ golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200827163409-021d7c6f1ec3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
76 changes: 76 additions & 0 deletions nodecheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package nodecheck

import (
"regexp"
"strings"

"github.com/vektah/gqlparser/v2/ast"

"github.com/gqlgo/gqlanalysis"
)

func Analyzer(excludes string) *gqlanalysis.Analyzer {
return &gqlanalysis.Analyzer{
Name: "nodecheck",
Doc: "nodecheck will find any GraphQL schema that is not conform to Node interface",
Run: run(excludes),
}
}

func run(excludes string) func(pass *gqlanalysis.Pass) (interface{}, error) {
return func(pass *gqlanalysis.Pass) (interface{}, error) {
allTypes := map[string]*ast.Definition{}
for _, def := range pass.Schema.Types {
if def.Kind == ast.Object {
allTypes[def.Name] = def
}
}

allNodeImplements := map[string]*ast.Definition{}
for name, t := range allTypes {
for _, typeInterface := range pass.Schema.Implements[name] {
if typeInterface.Kind == ast.Interface && typeInterface.Name == "Node" {
allNodeImplements[name] = t
}
}
}

unconformedTypes := map[string]*ast.Definition{}
for k, v := range allTypes {
if _, ok := allNodeImplements[k]; !ok {
unconformedTypes[v.Name] = v
}
}

needToNodeTypes := []*ast.Definition{}
for k, v := range unconformedTypes {
ok := false
for _, rule := range strings.Split(excludes, ",") {
if len(rule) > 0 {
regex := regexp.MustCompile(rule)

if ok {
break
}
if regex.MatchString(k) {
ok = true
}
}
}

if !ok {
needToNodeTypes = append(needToNodeTypes, v)
}
}

for _, t := range needToNodeTypes {
// Skip private type. e.g) __Directive, __Enum ...
if strings.HasPrefix(t.Name, "__") {
continue
}
pass.Reportf(t.Position, "%+v should conform to Node", t.Name)
}

return nil, nil
}
}
28 changes: 28 additions & 0 deletions nodecheck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package nodecheck_test

import (
"testing"

"github.com/bannzai/nodecheck"
"github.com/gqlgo/gqlanalysis/analysistest"
)

func Test(t *testing.T) {
testdata := analysistest.TestData(t)
analysistest.Run(t, testdata, nodecheck.Analyzer(""), "a")
}

func TestWithSingleExclusion(t *testing.T) {
testdata := analysistest.TestData(t)
analysistest.Run(t, testdata, nodecheck.Analyzer("Community"), "b")
}

func TestWithMultipleExclusion(t *testing.T) {
testdata := analysistest.TestData(t)
analysistest.Run(t, testdata, nodecheck.Analyzer("Community,Item"), "c")
}

func TestWithRegex(t *testing.T) {
testdata := analysistest.TestData(t)
analysistest.Run(t, testdata, nodecheck.Analyzer(".+Payload"), "d")
}
17 changes: 17 additions & 0 deletions testdata/a/schema/model.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface Node {
id: ID!
}

type User implements Node {
id: ID!
}

# Invalid. not conform to Node
type Community { # want "Community should conform to Node"
name: String!
}

# Invalid. not conform to Node but id is exist
type Item { # want "Item should conform to Node"
id: ID!
}
17 changes: 17 additions & 0 deletions testdata/b/schema/model.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface Node {
id: ID!
}

type User implements Node {
id: ID!
}

# Invalid. not conform to Node
type Community {
name: String!
}

# Invalid. not conform to Node but id is exist
type Item { # want "Item should conform to Node"
id: ID!
}
17 changes: 17 additions & 0 deletions testdata/c/schema/model.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface Node {
id: ID!
}

type User implements Node {
id: ID!
}

# Invalid. not conform to Node
type Community {
name: String!
}

# Invalid. not conform to Node but id is exist
type Item {
id: ID!
}
17 changes: 17 additions & 0 deletions testdata/d/schema/model.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface Node {
id: ID!
}

type User implements Node {
id: ID!
}

# Invalid. not conform to Node
type CommunityPayload {
name: String!
}

# Invalid. not conform to Node but id is exist
type ItemPayload {
id: ID!
}

0 comments on commit c129568

Please sign in to comment.