Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add json output format #5

Merged
merged 5 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
}
Loading