Skip to content

Commit

Permalink
Add json output format (#5)
Browse files Browse the repository at this point in the history
* Add a -json command-line flag for JSON output.

* Add JSON test.

* Add more coverage.

* Add Column to JSON output. Update Readme.
  • Loading branch information
bobg authored Jan 21, 2024
1 parent c958f04 commit e577662
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 14 deletions.
85 changes: 83 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
]
}
]
}
```
75 changes: 75 additions & 0 deletions cmd/decouple/decouple_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
97 changes: 85 additions & 12 deletions cmd/decouple/main.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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

Expand All @@ -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"`
}

0 comments on commit e577662

Please sign in to comment.