diff --git a/Readme.md b/Readme.md index 0c33daa..febf98b 100644 --- a/Readme.md +++ b/Readme.md @@ -55,16 +55,18 @@ go install github.com/bobg/decouple/cmd/decouple@latest ## Usage ```sh -decouple [-v] [DIR] +decouple [-v] [-json] [DIR] ``` This produces a report about the Go packages rooted at DIR (the current directory by default). With -v, very verbose debugging output is printed along the way. +With -json, +the output is in JSON format. The report will be empty if decouple has no findings. -Otherwise, it will look something like this: +Otherwise, it will look something like this (without -json): ``` $ decouple @@ -110,3 +112,82 @@ while the absence of them means “this is an existing type that already has the Decouple can’t always find a suitable existing type even when one exists, and if two or more types match, it doesn’t always choose the best one. + +The same report with `-json` specified looks like this: + +``` +{ + "PackageName": "main", + "FileName": "/home/bobg/kodigcs/handle.go", + "Line": 105, + "Column": 18, + "FuncName": "handleDir", + "Params": [ + { + "Name": "req", + "Methods": [ + "Context" + ] + }, + { + "Name": "w", + "Methods": [ + "Write" + ], + "InterfaceName": "io.Writer" + } + ] +} +{ + "PackageName": "main", + "FileName": "/home/bobg/kodigcs/handle.go", + "Line": 167, + "Column": 18, + "FuncName": "handleNFO", + "Params": [ + { + "Name": "req", + "Methods": [ + "Context" + ] + }, + { + "Name": "w", + "Methods": [ + "Header", + "Write" + ] + } + ] +} +{ + "PackageName": "main", + "FileName": "/home/bobg/kodigcs/handle.go", + "Line": 428, + "Column": 6, + "FuncName": "isStale", + "Params": [ + { + "Name": "t", + "Methods": [ + "Before" + ] + } + ] +} +{ + "PackageName": "main", + "FileName": "/home/bobg/kodigcs/imdb.go", + "Line": 59, + "Column": 6, + "FuncName": "parseIMDbPage", + "Params": [ + { + "Name": "cl", + "Methods": [ + "Do" + ] + } + ] +} +``` diff --git a/cmd/decouple/decouple_test.go b/cmd/decouple/decouple_test.go new file mode 100644 index 0000000..13b89db --- /dev/null +++ b/cmd/decouple/decouple_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "bytes" + "encoding/json" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/bobg/go-generics/v3/iter" +) + +func TestRunJSON(t *testing.T) { + buf := new(bytes.Buffer) + if err := run(buf, false, true, []string{"../.."}); err != nil { + t.Fatal(err) + } + + var ( + got []jtuple + dec = json.NewDecoder(buf) + ) + for dec.More() { + var val jtuple + if err := dec.Decode(&val); err != nil { + t.Fatal(err) + } + val.FileName = filepath.Base(val.FileName) + got = append(got, val) + } + + want := []jtuple{{ + PackageName: "main", + FileName: "main.go", + Line: 100, + Column: 6, + FuncName: "showJSON", + Params: []jparam{{ + Name: "checker", + Methods: []string{ + "NameForMethods", + }, + }}, + }} + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, want %v", got, want) + } +} + +func TestRunPlain(t *testing.T) { + buf := new(bytes.Buffer) + if err := run(buf, false, false, []string{"../.."}); err != nil { + t.Fatal(err) + } + + lines, err := iter.ToSlice(iter.Lines(buf)) + if err != nil { + t.Fatal(err) + } + + if len(lines) != 2 { + t.Fatalf("got %d lines, want 2", len(lines)) + } + if !strings.HasSuffix(lines[0], ": showJSON") { + t.Fatalf(`line 1 is "%s", want something ending in ": showJSON"`, lines[0]) + } + + lines[1] = strings.TrimSpace(lines[1]) + const want = "checker: [NameForMethods]" + if lines[1] != want { + t.Fatalf(`line 2 is "%s", want "%s"`, lines[1], want) + } +} diff --git a/cmd/decouple/main.go b/cmd/decouple/main.go index 5ca860e..ad3da39 100644 --- a/cmd/decouple/main.go +++ b/cmd/decouple/main.go @@ -1,43 +1,54 @@ package main import ( + "encoding/json" "flag" "fmt" + "io" "os" "sort" + "github.com/bobg/errors" "github.com/bobg/go-generics/v3/maps" "github.com/bobg/decouple" ) func main() { - var verbose bool + var ( + verbose bool + doJSON bool + ) flag.BoolVar(&verbose, "v", false, "verbose") + flag.BoolVar(&doJSON, "json", false, "output in JSON format") flag.Parse() + if err := run(os.Stdout, verbose, doJSON, flag.Args()); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(w io.Writer, verbose, doJSON bool, args []string) error { var dir string - switch flag.NArg() { + switch len(args) { case 0: dir = "." case 1: - dir = flag.Arg(0) + dir = args[0] default: - fmt.Fprintf(os.Stderr, "Usage: %s [-v] [DIR]\n", os.Args[0]) - os.Exit(1) + return fmt.Errorf("Usage: %s [-v] [-json] [DIR]", os.Args[0]) } checker, err := decouple.NewCheckerFromDir(dir) if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + return errors.Wrapf(err, "creating checker for %s", dir) } checker.Verbose = verbose tuples, err := checker.Check() if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + return errors.Wrapf(err, "checking %s", dir) } sort.Slice(tuples, func(i, j int) bool { @@ -51,6 +62,11 @@ func main() { return iPos.Offset < jPos.Offset }) + if doJSON { + err := showJSON(w, checker, tuples) + return errors.Wrap(err, "formatting JSON output") + } + for _, tuple := range tuples { var showedFuncName bool @@ -63,18 +79,75 @@ func main() { } if !showedFuncName { - fmt.Printf("%s: %s\n", tuple.Pos(), tuple.F.Name.Name) + fmt.Fprintf(w, "%s: %s\n", tuple.Pos(), tuple.F.Name.Name) showedFuncName = true } if intfName := checker.NameForMethods(mm); intfName != "" { - fmt.Printf(" %s: %s\n", param, intfName) + fmt.Fprintf(w, " %s: %s\n", param, intfName) continue } methods := maps.Keys(tuple.M[param]) sort.Strings(methods) - fmt.Printf(" %s: %v\n", param, methods) + fmt.Fprintf(w, " %s: %v\n", param, methods) } } + + return nil +} + +func showJSON(w io.Writer, checker decouple.Checker, tuples []decouple.Tuple) error { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + + for _, tuple := range tuples { + p := tuple.Pos() + jt := jtuple{ + PackageName: tuple.P.Name, + FileName: p.Filename, + Line: p.Line, + Column: p.Column, + FuncName: tuple.F.Name.Name, + } + for param, mm := range tuple.M { + if len(mm) == 0 { + continue + } + jp := jparam{ + Name: param, + Methods: maps.Keys(mm), + } + sort.Strings(jp.Methods) + if intfName := checker.NameForMethods(mm); intfName != "" { + jp.InterfaceName = intfName + } + jt.Params = append(jt.Params, jp) + } + if len(jt.Params) == 0 { + continue + } + sort.Slice(jt.Params, func(i, j int) bool { + return jt.Params[i].Name < jt.Params[j].Name + }) + if err := enc.Encode(jt); err != nil { + return err + } + } + + return nil +} + +type jtuple struct { + PackageName string + FileName string + Line, Column int + FuncName string + Params []jparam +} + +type jparam struct { + Name string + Methods []string `json:",omitempty"` + InterfaceName string `json:",omitempty"` }