├── README.md ├── format.go ├── go.mod ├── go.sum └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # gqlfmt 2 | 3 | `gqlfmt` formats your GraphQL files. 4 | 5 | ## Install 6 | 7 | ``` 8 | go install github.com/gqlgo/gqlfmt@latest 9 | ``` 10 | 11 | ## Usage 12 | 13 | - Help 14 | ``` 15 | $ gqlfmt --help 16 | usage: gqlfmt [flags] [path ...] 17 | -d display diffs instead of rewriting files 18 | -l list files whose formatting differs from gqlfmt's 19 | -v verbose logging 20 | -w write result to (source) file instead of stdout 21 | ``` 22 | 23 | - to stdout 24 | ```sh 25 | $ cat *.graphql # display unformatted files 26 | ``` 27 | 28 | ```graphql 29 | query A1 { 30 | id 31 | test 32 | } 33 | 34 | query A2 { 35 | id 36 | test 37 | } 38 | query B1 { 39 | id 40 | hello 41 | } 42 | 43 | query B2 { 44 | id 45 | 46 | 47 | 48 | test 49 | } 50 | query C1 { 51 | id 52 | } 53 | ``` 54 | 55 | ```bash 56 | $ gqlfmt *.graphql 57 | ``` 58 | 59 | ```graphql 60 | query A1 { 61 | id 62 | test 63 | } 64 | query A2 { 65 | id 66 | test 67 | } 68 | query B1 { 69 | id 70 | hello 71 | } 72 | query B2 { 73 | id 74 | test 75 | } 76 | query C1 { 77 | id 78 | } 79 | ``` 80 | 81 | - Over write GraphQL files 82 | ```sh 83 | $ gqlfmt -w *.graphql 84 | ``` 85 | 86 | - List files whose formatting differs from gqlfmt's 87 | ```sh 88 | $ gqlfmt -l *.graphql 89 | a.graphql 90 | b.graphql 91 | ``` 92 | 93 | - Display diffs 94 | ```sh 95 | $ gqlfmt -d *.graphql 96 | ``` 97 | 98 | ```diff 99 | diff -u a.graphql.orig a.graphql 100 | --- a.graphql.orig 2021-04-06 14:26:26.000000000 +0900 101 | +++ a.graphql 2021-04-06 14:26:26.000000000 +0900 102 | @@ -1,9 +1,8 @@ 103 | query A1 { 104 | - id 105 | - test 106 | + id 107 | + test 108 | } 109 | - 110 | query A2 { 111 | - id 112 | - test 113 | + id 114 | + test 115 | } 116 | diff -u b.graphql.orig b.graphql 117 | --- b.graphql.orig 2021-04-06 14:26:26.000000000 +0900 118 | +++ b.graphql 2021-04-06 14:26:26.000000000 +0900 119 | @@ -1,12 +1,8 @@ 120 | query B1 { 121 | - id 122 | - hello 123 | + id 124 | + hello 125 | } 126 | - 127 | query B2 { 128 | - id 129 | - 130 | - 131 | - 132 | - test 133 | + id 134 | + test 135 | } 136 | ``` 137 | 138 | - from stdin 139 | 140 | ```sh 141 | $ cat a.graphql | gqlfmt -d 142 | ``` 143 | 144 | ```diff 145 | diff -u stdin.go.orig stdin.go 146 | --- stdin.go.orig 2021-04-06 15:03:59.000000000 +0900 147 | +++ stdin.go 2021-04-06 15:03:59.000000000 +0900 148 | @@ -1,9 +1,8 @@ 149 | query A1 { 150 | - id 151 | - test 152 | + id 153 | + test 154 | } 155 | - 156 | query A2 { 157 | - id 158 | - test 159 | + id 160 | + test 161 | } 162 | ``` 163 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/vektah/gqlparser/v2/ast" 8 | "github.com/vektah/gqlparser/v2/formatter" 9 | "github.com/vektah/gqlparser/v2/parser" 10 | ) 11 | 12 | func process(filename string, src []byte, opt *Options) ([]byte, error) { 13 | source := &ast.Source{Name: filename, Input: string(src)} 14 | 15 | query, err := parser.ParseQuery(source) 16 | if err == nil { 17 | return queryFormat(query), nil 18 | } 19 | 20 | schema, err := parser.ParseSchema(source) 21 | if err == nil { 22 | return schemaFormat(schema), nil 23 | } 24 | 25 | return nil, fmt.Errorf("%v is not GraphQL file: %w", filename, err) 26 | } 27 | 28 | func queryFormat(queryDocument *ast.QueryDocument) []byte { 29 | var buf bytes.Buffer 30 | astFormatter := formatter.NewFormatter(&buf) 31 | astFormatter.FormatQueryDocument(queryDocument) 32 | return buf.Bytes() 33 | } 34 | 35 | func schemaFormat(schemaDocument *ast.SchemaDocument) []byte { 36 | var buf bytes.Buffer 37 | astFormatter := formatter.NewFormatter(&buf) 38 | astFormatter.FormatSchemaDocument(schemaDocument) 39 | return buf.Bytes() 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gqlgo/gqlfmt 2 | 3 | go 1.19 4 | 5 | require github.com/vektah/gqlparser/v2 v2.5.10 6 | 7 | require ( 8 | github.com/stretchr/testify v1.7.0 // indirect 9 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= 2 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 10 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= 12 | github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 17 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "go/scanner" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "runtime" 16 | "strings" 17 | ) 18 | 19 | type Options struct { 20 | TabWidth int 21 | TabIndent bool 22 | Comments bool 23 | } 24 | 25 | var ( 26 | // main operation modes 27 | list = flag.Bool("l", false, "list files whose formatting differs from gqlfmt's") 28 | write = flag.Bool("w", false, "write result to (source) file instead of stdout") 29 | doDiff = flag.Bool("d", false, "display diffs instead of rewriting files") 30 | 31 | verbose bool // verbose logging 32 | 33 | options = &Options{ 34 | TabWidth: 4, 35 | TabIndent: true, 36 | Comments: true, 37 | } 38 | 39 | exitCode = 0 40 | ) 41 | 42 | func report(err error) { 43 | scanner.PrintError(os.Stderr, err) 44 | exitCode = 2 45 | } 46 | 47 | func usage() { 48 | fmt.Fprintf(os.Stderr, "usage: gqlfmt [flags] [path ...]\n") 49 | flag.PrintDefaults() 50 | os.Exit(2) 51 | } 52 | 53 | func isGQLFile(f os.FileInfo) bool { 54 | name := f.Name() 55 | return !f.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".graphql") 56 | } 57 | 58 | // argumentType is which mode gqlfmt was invoked as. 59 | type argumentType int 60 | 61 | const ( 62 | // fromStdin means the user is piping their source into gqlfmt. 63 | fromStdin argumentType = iota 64 | 65 | // singleArg is the common case from editors, when gqlfmt is run on 66 | // a single file. 67 | singleArg 68 | 69 | // multipleArg is when the user ran "gqlfmt file1.go file2.go" 70 | // or ran gqlfmt on a directory tree. 71 | multipleArg 72 | ) 73 | 74 | func processFile(filename string, in io.Reader, out io.Writer, argType argumentType) error { 75 | opt := options 76 | if argType == fromStdin { 77 | nopt := *options 78 | opt = &nopt 79 | } 80 | 81 | if in == nil { 82 | f, err := os.Open(filename) 83 | if err != nil { 84 | return err 85 | } 86 | defer f.Close() 87 | in = f 88 | } 89 | 90 | src, err := ioutil.ReadAll(in) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | res, err := process(filename, src, opt) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if !bytes.Equal(src, res) { 101 | // formatting has changed 102 | if *list { 103 | fmt.Fprintln(out, filename) 104 | } 105 | if *write { 106 | if argType == fromStdin { 107 | // filename is "" 108 | return errors.New("can't use -w on stdin") 109 | } 110 | err = ioutil.WriteFile(filename, res, 0) 111 | if err != nil { 112 | return err 113 | } 114 | } 115 | if *doDiff { 116 | if argType == fromStdin { 117 | filename = "stdin.go" // because .orig looks silly 118 | } 119 | data, err := diff(src, res, filename) 120 | if err != nil { 121 | return fmt.Errorf("computing diff: %s", err) 122 | } 123 | fmt.Printf("diff -u %s %s\n", filepath.ToSlash(filename+".orig"), filepath.ToSlash(filename)) 124 | out.Write(data) 125 | } 126 | } 127 | 128 | if !*list && !*write && !*doDiff { 129 | _, err = out.Write(res) 130 | } 131 | 132 | return err 133 | } 134 | 135 | func visitFile(path string, f os.FileInfo, err error) error { 136 | if err == nil && isGQLFile(f) { 137 | err = processFile(path, nil, os.Stdout, multipleArg) 138 | } 139 | if err != nil { 140 | report(err) 141 | } 142 | return nil 143 | } 144 | 145 | func walkDir(path string) { 146 | filepath.Walk(path, visitFile) 147 | } 148 | 149 | func main() { 150 | runtime.GOMAXPROCS(runtime.NumCPU()) 151 | 152 | // call gofmtMain in a separate function 153 | // so that it can use defer and have them 154 | // run before the exit. 155 | gqlfmtMain() 156 | os.Exit(exitCode) 157 | } 158 | 159 | // parseFlags parses command line flags and returns the paths to process. 160 | // It's a var so that custom implementations can replace it in other files. 161 | var parseFlags = func() []string { 162 | flag.BoolVar(&verbose, "v", false, "verbose logging") 163 | 164 | flag.Parse() 165 | return flag.Args() 166 | } 167 | 168 | func gqlfmtMain() { 169 | flag.Usage = usage 170 | paths := parseFlags() 171 | 172 | if verbose { 173 | log.SetFlags(log.LstdFlags | log.Lmicroseconds) 174 | } 175 | if options.TabWidth < 0 { 176 | fmt.Fprintf(os.Stderr, "negative tabwidth %d\n", options.TabWidth) 177 | exitCode = 2 178 | return 179 | } 180 | 181 | if len(paths) == 0 { 182 | if err := processFile("", os.Stdin, os.Stdout, fromStdin); err != nil { 183 | report(err) 184 | } 185 | return 186 | } 187 | 188 | argType := singleArg 189 | if len(paths) > 1 { 190 | argType = multipleArg 191 | } 192 | 193 | for _, path := range paths { 194 | switch dir, err := os.Stat(path); { 195 | case err != nil: 196 | report(err) 197 | case dir.IsDir(): 198 | walkDir(path) 199 | default: 200 | if err := processFile(path, nil, os.Stdout, argType); err != nil { 201 | report(err) 202 | } 203 | } 204 | } 205 | } 206 | 207 | func writeTempFile(dir, prefix string, data []byte) (string, error) { 208 | file, err := ioutil.TempFile(dir, prefix) 209 | if err != nil { 210 | return "", err 211 | } 212 | _, err = file.Write(data) 213 | if err1 := file.Close(); err == nil { 214 | err = err1 215 | } 216 | if err != nil { 217 | os.Remove(file.Name()) 218 | return "", err 219 | } 220 | return file.Name(), nil 221 | } 222 | 223 | func diff(b1, b2 []byte, filename string) (data []byte, err error) { 224 | f1, err := writeTempFile("", "gofmt", b1) 225 | if err != nil { 226 | return 227 | } 228 | defer os.Remove(f1) 229 | 230 | f2, err := writeTempFile("", "gofmt", b2) 231 | if err != nil { 232 | return 233 | } 234 | defer os.Remove(f2) 235 | 236 | cmd := "diff" 237 | if runtime.GOOS == "plan9" { 238 | cmd = "/bin/ape/diff" 239 | } 240 | 241 | data, err = exec.Command(cmd, "-u", f1, f2).CombinedOutput() 242 | if len(data) > 0 { 243 | // diff exits with a non-zero status when the files don't match. 244 | // Ignore that failure as long as we get output. 245 | return replaceTempFilename(data, filename) 246 | } 247 | return 248 | } 249 | 250 | // replaceTempFilename replaces temporary filenames in diff with actual one. 251 | // 252 | // --- /tmp/gofmt316145376 2017-02-03 19:13:00.280468375 -0500 253 | // +++ /tmp/gofmt617882815 2017-02-03 19:13:00.280468375 -0500 254 | // ... 255 | // -> 256 | // --- path/to/file.go.orig 2017-02-03 19:13:00.280468375 -0500 257 | // +++ path/to/file.go 2017-02-03 19:13:00.280468375 -0500 258 | // ... 259 | func replaceTempFilename(diff []byte, filename string) ([]byte, error) { 260 | bs := bytes.SplitN(diff, []byte{'\n'}, 3) 261 | if len(bs) < 3 { 262 | return nil, fmt.Errorf("got unexpected diff for %s", filename) 263 | } 264 | // Preserve timestamps. 265 | var t0, t1 []byte 266 | if i := bytes.LastIndexByte(bs[0], '\t'); i != -1 { 267 | t0 = bs[0][i:] 268 | } 269 | if i := bytes.LastIndexByte(bs[1], '\t'); i != -1 { 270 | t1 = bs[1][i:] 271 | } 272 | // Always print filepath with slash separator. 273 | f := filepath.ToSlash(filename) 274 | bs[0] = []byte(fmt.Sprintf("--- %s%s", f+".orig", t0)) 275 | bs[1] = []byte(fmt.Sprintf("+++ %s%s", f, t1)) 276 | return bytes.Join(bs, []byte{'\n'}), nil 277 | } 278 | --------------------------------------------------------------------------------