Skip to content

Gadget is a tool that allows you to quickly inspect your Go source code. It's effectively a small layer of abstraction built on top of the Go AST package.

License

Notifications You must be signed in to change notification settings

wilhelm-murdoch/go-gadget

Repository files navigation

Gadget

CI Status Release Status GoDoc Go report Stability: Active

gadget is a tool that allows you to quickly inspect your Go source code. It's effectively a small layer of abstraction built on top of the Go AST package.

It inspects your go code, hence the name: Go-go Gadget!

Why?

I was working on another project of mine and thought to myself, "It would be nice if I didn't have to constantly update this readme file every time I made a change." So, I started digging around Go's AST package and came up with gadget.

But, pkg.go.dev already does this ...

Yeah, I know. But, I didn't fully realise I was writing a crappier version of pkg.go.dev until about 90% into the project.

  • Maybe you don't want people to leave your repository to understand the basics of your package's API.
  • Maybe you want to present this data in a different, more personalised, format.
  • Maybe you can use this to write a basic linter, or just learn more about Go AST.

It was fun to write and I use the tool almost daily. Perhaps you'll find it useful as well.

Download & Install

Binary releases are regularly published for the most common operating systems and CPU architectures. These can be downloaded from the releases page. Presentingly, gadget has been tested on, and compiled for, the following:

  1. Windows on 386, arm, amd64
  2. MacOS ( debian ) on amd64, arm64
  3. Linux on 386, arm, amd64, arm64

Download the appropriate archive and unpack the binary into your machine's local $PATH.

Usage

Once added to your machine's local $PATH you can invoke gadget like so:

$ gadget --help
NAME:
   gadget - inspect your code via a small layer of abstraction over Go's AST package

USAGE:
   gadget [global options] command [command options] [arguments...]

VERSION:
   v0.0.12

AUTHOR:
   Wilhelm Murdoch <wilhelm@devilmayco.de>

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --source value, -s value    path to the target go source file, or directory containing go source files. (default: ".")
   --format value, -f value    the output format of the results as json, template or debug. (default: "json")
   --template value, -t value  if the template format is selected, this is the path to the template file to use. (default: "README.tpl")
   --help, -h                  show help (default: false)
   --version, -v               print only the version (default: false)

COPYRIGHT:
   (c) 2022 Wilhelm Codes ( https://wilhelm.codes )

Example Mapping

One of the primary goals of this project was to link example functions to their associated functions and methods. As I'm regenerating templates, I'd like to have the example body as well as the expected output to live alongside the associated function so I can easily reference it within the Go template.

JSON

Invoking the command with no flags will result in gadget searching for *.go files by recursively walking through the present working directory. Results will be displayed as a JSON object following this structure:

  • packages: a list of discovered packages.
    • files: any *.go file associated with the package.
      • types: discovered types, eg; structs, interfaces, etc...
        • fields: a list of fields associated with each type.
      • functions: functions and methods
        • examples: mapped example functions ( if any )
      • values: explicitly-declared values, eg; constants and variables

A full example of the JSON object can be found in here.

Debug

When invoking gadget using the --format debug flag, you will get output representing all evaluated source code using ast.Print(...). Use this to follow the structure of the AST.

$ gadget --source /path/to/my/project --format debug
... heaps of AST output ...
1298  .  .  15: *ast.FuncDecl {
1299  .  .  .  Doc: *ast.CommentGroup {
1300  .  .  .  .  List: []*ast.Comment (len = 1) {
1301  .  .  .  .  .  0: *ast.Comment {
1302  .  .  .  .  .  .  Slash: sink/sink.go:81:1
1303  .  .  .  .  .  .  Text: "// GetPrivate is an accessor method that returns a dark secret:"
1304  .  .  .  .  .  }
1305  .  .  .  .  }
1306  .  .  .  }
1307  .  .  .  Recv: *ast.FieldList {
1308  .  .  .  .  Opening: sink/sink.go:82:6
1309  .  .  .  .  List: []*ast.Field (len = 1) {
1310  .  .  .  .  .  0: *ast.Field {
1311  .  .  .  .  .  .  Names: []*ast.Ident (len = 1) {
1312  .  .  .  .  .  .  .  0: *ast.Ident {
1313  .  .  .  .  .  .  .  .  NamePos: sink/sink.go:82:7
1314  .  .  .  .  .  .  .  .  Name: "nst"
1315  .  .  .  .  .  .  .  .  Obj: *ast.Object {
1316  .  .  .  .  .  .  .  .  .  Kind: var
1317  .  .  .  .  .  .  .  .  .  Name: "nst"
1318  .  .  .  .  .  .  .  .  .  Decl: *(obj @ 1310)
1319  .  .  .  .  .  .  .  .  }
1320  .  .  .  .  .  .  .  }
1321  .  .  .  .  .  .  }
... heaps more AST output

Template

Use Go's template engine, along with sprig, to generate technical documents or readme files ( like this one! ).

$ gadget --format template --template README.tpl > README.md

Or, without the --template ... flag as it will use README.tpl as the default template if it exists in the starting directory:

$ gadget --format template > README.md

The best way to understand this is by viewing the following "kitchen sink" examples:

  1. sink/README.tpl is a valid Go template.
  2. sink/README.md was generated using the specified Go template.

Build Locally

gadget makes use of Go's new generics support, so the minimum viable version of the language is 1.18.x. Ensure your local development environment meets this single requirement before continuing. There are also several build flags used when compiling the binary. These populate the output of the gadget --version flag.

$ git clone git@github.com:wilhelm-murdoch/go-gadget.git
$ cd gadget
$ make build
$ ./gadget --version
Version: v99.99.99, Stage: local, Commit: 9932cf9fdc90c0d8223ef85a0fc1ddfa99c28f95, Date: 10-04-2022

Testing

All major functionality of gadget has been covered by testing. You can run the tests, benchmarks and lints using the following set of Makefile targets:

  • make test: run the local testing suite.
  • make lint: run staticcheck on the local source files.
  • make bench: run a series of benchmarks and output the results as cpu.out, mem.out and trace.out
  • make pprof-cpu: run a local webserver on port :8800 displaying CPU usage stats.
  • make pprof-mem: run a local webserver on port :8900 displaying memory usage stats.
  • make trace: view local tracing output from the benchmark run.
  • make coverage: view testing code coverage for the local source files.

API

While gadget is meant to be used as a CLI, there's no reason you can't make use of it as a library to integrate into your own tools. If you were wondering, yes, this readme file was generated by gadget itself.

File field.go

Type Field

  • type Field struct #
  • field.go:11:21 #

Exported Fields:

  1. Name: The name of the field. #
  2. IsExported: Determines whether the field is exported. #
  3. IsEmbedded: Determines whether the field is an embedded type. #
  4. Line: The line number this field appears on in the associated source file. #
  5. Signature: The full definition of the field including name, arguments and return values. #
  6. Comment: Any inline comments associated with the field. #
  7. Doc: The comment block directly above this field's definition. #

Function NewField

  • func NewField(f *ast.Field, parent *File) *Field #
  • field.go:25:33 #

NewField returns a field instance and attempts to populate all associatedfields with meaningful values.


Function Parse

  • func (f *Field) Parse() *Field #
  • field.go:37:56 #

Parse is responsible for browsing through f.astField, f.parent to populatethe current fields's fields. ( Chainable )


Function parseSignature

  • func (f *Field) parseSignature() #
  • field.go:62:70 #

parseSignature determines the position of the current field within theassociated source file and extracts the relevant line of code. We only wantthe content before any inline comments. This will also replace consecutivespaces with a single space.


Function String

  • func (f *Field) String() string #
  • field.go:73:75 #

String implements the Stringer struct and returns the current package's name.


File file.go

Type File

  • type File struct #
  • file.go:14:29 #

Exported Fields:

  1. Name: The basename of the file. #
  2. Path: The full path to the file as specified by the caller. #
  3. Package: The name of the golang package associated with this file. #
  4. IsMain: Determines whether this file is part of package main. #
  5. IsTest: Determines whether this file is for golang tests. #
  6. HasTests: Determines whether this file contains golang tests. #
  7. HasBenchmarks: Determines whether this file contains benchmark tests. #
  8. HasExamples: Determines whether this file contains example tests. #
  9. Imports: A list of strings containing all the current file's package imports. #
  10. Values: A collection of declared golang values. #
  11. Functions: A collection of declared golang functions. #
  12. Types: A collection of declared golang types. #

Function NewFile

  • func NewFile(path string) (*File, error) #
  • file.go:34:50 #

NewFile returns a file instance representing a file within a golang package.This function creates a new token set and parser instance representing thenew file's abstract syntax tree ( AST ).

Examples:

package main

import (
  "fmt"
  "strings"

  "github.com/wilhelm-murdoch/go-gadget"
)

func main() {
    if file, err := gadget.NewFile("sink/sink.go"); err == nil {
    	file.Functions.Each(func(i int, function *gadget.Function) bool {
    		fmt.Printf("%s defined between lines %d and %d\n", function.Name, function.LineStart, function.LineEnd)
    		return false
    	})
    }
}
// Output:
// PrintVars defined between lines 30 and 34
// AssignCollection defined between lines 37 and 43
// PrintConst defined between lines 46 and 50
// NewNormalStructTest defined between lines 72 and 79
// GetPrivate defined between lines 82 and 84
// GetOccupation defined between lines 87 and 89
// GetFullName defined between lines 92 and 94
// notExported defined between lines 98 and 100
// NewGenericStructTest defined between lines 111 and 113
// GetPrivate defined between lines 116 and 118
// GetFullName defined between lines 121 and 123
// IsBlank defined between lines 126 and 126
package main

import (
  "fmt"
  "strings"

  "github.com/wilhelm-murdoch/go-gadget"
)

func main() {
    if file, err := gadget.NewFile("sink/sink.go"); err == nil {
    	file.Types.Each(func(i int, t *gadget.Type) bool {
    		if t.Fields.Length() > 0 {
    			fmt.Printf("%s is a %s with %d fields:\n", t.Name, t.Kind, t.Fields.Length())
    			t.Fields.Each(func(i int, f *gadget.Field) bool {
    				fmt.Printf("- %s on line %d\n", f.Name, f.Line)
    				return false
    			})
    		}
    		return false
    	})
    }
}
// Output:
// InterfaceTest is a interface with 1 fields:
// - ImplementMe on line 54
// EmbeddedStructTest is a struct with 1 fields:
// - Occupation on line 59
// NormalStructTest is a struct with 5 fields:
// - First on line 64
// - Last on line 65
// - Age on line 66
// - private on line 67
// - EmbeddedStructTest on line 68
// GenericStructTest is a struct with 4 fields:
// - First on line 104
// - Last on line 105
// - Age on line 106
// - private on line 107
package main

import (
  "fmt"
  "strings"

  "github.com/wilhelm-murdoch/go-gadget"
)

func main() {
    var buffer strings.Builder
    if file, err := gadget.NewFile("sink/sink.go"); err == nil {
    	encoder := json.NewEncoder(&buffer)
    	if err := encoder.Encode(file.Values.Items()); err != nil {
    		fmt.Println(err)
    	}
    }
    
    fmt.Println(buffer.String())
}
// Output:
// [{"kind":"const","name":"ONE","line":9,"body":"ONE   = 1 // represents the number 1"},{"kind":"const","name":"TWO","line":10,"body":"TWO   = 2 // represents the number 2"},{"kind":"const","name":"THREE","line":11,"body":"THREE = 3 // represents the number 3"},{"kind":"var","name":"one","line":16,"body":"one   = \"one\"   // represents the english spelling of 1"},{"kind":"var","name":"two","line":17,"body":"two   = \"two\"   // represents the english spelling of 2"},{"kind":"var","name":"three","line":18,"body":"three = \"three\" // represents the english spelling of 3"},{"kind":"var","name":"collection","line":27,"body":"var collection map[string]map[string]string // this should be picked up as an inline comment."}]

Function Parse

  • func (f *File) Parse() *File #
  • file.go:56:68 #

Parse is responsible for walking through the current file's abstract syntaxtree in order to populate it's fields. This includes imports, definedfunctions and methods, structs and interfaces and other declared values.( Chainable )


Function parsePackage

  • func (f *File) parsePackage() #
  • file.go:71:76 #

parsePackage updates the current file with package-related data.


Function parseImports

  • func (f *File) parseImports() #
  • file.go:81:85 #

parseImports is responsible for creating a list of package imports that havebeen defined within the current file and assinging them to the appropriateImports field.


Function parseFunctions

  • func (f *File) parseFunctions() #
  • file.go:90:106 #

parseFunctions is responsible for creating abstract representations offunctions and methods defined within the current file. All functions areadded to the Functions collection.


Function parseTypes

  • func (f *File) parseTypes() #
  • file.go:111:119 #

parseTypes is responsible for creating abstract representations of declaredgolang types defined within the current file. All findings are added to theTypes collection.


Function parseValues

  • func (f *File) parseValues() #
  • file.go:124:134 #

parseValues is responsible for creating abstract representations of variousgeneral values such as const and var blocks. All values are added to theValues collection.


Function walk

  • func (f *File) walk(fn func(ast.Node) bool) #
  • file.go:138:140 #

walk implements the walk interface which is used to step through syntaxtrees via a caller-supplied callback.


Function String

  • func (f *File) String() string #
  • file.go:143:145 #

String implements the Stringer struct and returns the current package's name.


Function GetAstAttributes

  • func (f *File) GetAstAttributes() (*ast.File, *token.FileSet) #
  • file.go:149:151 #

GetAstAttributes returns the values associated with the astFile and tokenSetprivate fields. This is typically used for debug mode.


File function.go

Type Function

  • type Function struct #
  • function.go:16:35 #

Exported Fields:

  1. Name: The name of the function. #
  2. IsTest: Determines whether this is a test. #
  3. IsBenchmark: Determines whether this is a benchmark. #
  4. IsExample: Determines whether this is an example. #
  5. IsExported: Determines whether this function is exported. #
  6. IsMethod: Determines whether this a method. This will be true if this function has a receiver. #
  7. Receiver: If this method has a receiver, this field will refer to the name of the associated struct. #
  8. Doc: The comment block directly above this funciton's definition. #
  9. Output: If IsExample is true, this field should contain the comment block defining expected output. #
  10. Body: The body of this function; everything contained within the opening and closing braces. #
  11. Signature: The full definition of the function including receiver, name, arguments and return values. #
  12. LineStart: The line number in the associated source file where this function is initially defined. #
  13. LineEnd: The line number in the associated source file where the definition block ends. #
  14. LineCount: The total number of lines, including body, the interface occupies. #
  15. Examples: A list of example functions associated with the current function. #

Function NewFunction

  • func NewFunction(fn *ast.FuncDecl, parent *File) *Function #
  • function.go:39:47 #

NewFunction returns a function instance and attempts to populate allassociated fields with meaningful values.


Function Parse

  • func (f *Function) Parse() *Function #
  • function.go:51:72 #

Parse is responsible for browsing through f.astFunc, f.tokenSet and f.astFileto populate the current function's fields. ( Chainable )


Function parseReceiver

  • func (f *Function) parseReceiver() #
  • function.go:76:95 #

parseReceiver attemps to assign the receiver of a method, if one even exists,and assigns it to the Function.Receiver field.


Function parseOutput

  • func (f *Function) parseOutput() #
  • function.go:101:121 #

parseOutput attempts to fetch the expected output block for an examplefunction and pins it to the current Function for future reference. It assumesall comments immediately following the position of string "// Output:"belong to the output block.


Function parseLines

  • func (f *Function) parseLines() #
  • function.go:125:129 #

parseLines determines the current function body's line positions within thecurrently evaluated file.


Function parseBody

  • func (f *Function) parseBody() #
  • function.go:136:145 #

parseBody attempts to make a few adjustments to the *ast.BlockStmt whichrepresents the current function's body. We remove the opening and closingbraces as well as the first occurrent \t sequence on each line. Some peoplewill ask, "wHy dOn't yOu uSe tHe aSt pAcKaGe fOr tHiS" to which I answer,"Because, I'm lazy. We have the file, we know which lines contain the body."


Function parseSignature

  • func (f *Function) parseSignature() #
  • function.go:149:152 #

parseSignature attempts to determine the current function's type and assignsit to the Signature field of struct Function.


Function String

  • func (f *Function) String() string #
  • function.go:155:157 #

String implements the Stringer struct and returns the current package's name.


File package.go

Type Package

  • type Package struct #
  • package.go:9:12 #

Exported Fields:

  1. Name: The name of the current package. #
  2. Files: A collection of golang files associated with this package. #---

Function NewPackage

  • func NewPackage(name string) *Package #
  • package.go:16:21 #

NewPackage returns a Package instance with an initialised collection used forassigning and iterating through files.


Function String

  • func (p *Package) String() string #
  • package.go:24:26 #

String implements the Stringer struct and returns the current package's name.


File type.go

Type Type

  • type Type struct #
  • type.go:20:33 #

Exported Fields:

  1. Name: The name of the struct. #
  2. Kind: Determines the kind of type, eg; interface or struct. #
  3. LineStart: The line number in the associated source file where this struct is initially defined. #
  4. LineEnd: The line number in the associated source file where the definition block ends. #
  5. LineCount: The total number of lines, including body, the struct occupies. #
  6. Comment: Any inline comments associated with the struct. #
  7. Doc: The comment block directly above this struct's definition. #
  8. Signature: The full definition of the struct itself. #
  9. Body: The full body of the struct sourced directly from the associated file; comments included. #
  10. Fields: A collection of fields and their associated metadata. #

Function NewType

  • func NewType(ts *ast.TypeSpec, parent *File) *Type #
  • type.go:37:45 #

NewType returns an struct instance and attempts to populate all associatedfields with meaningful values.


Function Parse

  • func (t *Type) Parse() *Type #
  • type.go:49:56 #

Parse is responsible for browsing through f.astSpec, f.astType, f.parent topopulate the current struct's fields. ( Chainable )


Function parseLines

  • func (t *Type) parseLines() #
  • type.go:60:64 #

parseLines determines the current struct's opening and closing linepositions.


Function parseBody

  • func (t *Type) parseBody() #
  • type.go:69:71 #

parseBody attempts to make a few adjustments to the *ast.BlockStmt whichrepresents the current struct's body. We remove the opening and closingbraces as well as the first occurrent \t sequence on each line.


Function parseSignature

  • func (t *Type) parseSignature() #
  • type.go:75:78 #

parseSignature attempts to determine the current structs's type and assignsit to the Signature field of struct Function.


Function parseFields

  • func (t *Type) parseFields() #
  • type.go:82:103 #

parseFields iterates through the struct's list of defined methods topopulate the Fields collection.


Function String

  • func (t *Type) String() string #
  • type.go:106:108 #

String implements the Stringer struct and returns the current package's name.


File util.go

Type Walker

  • type Walker func(ast.Node) boo #
  • util.go:80:80 #

Function GetLinesFromFile

  • func GetLinesFromFile(path string, from, to int) []byte #
  • util.go:18:46 #

GetLinesFromFile creates a byte reader for the file at the target path andreturns a slice of bytes representing the file content. This slice isrestricted the lines specified by the from and to arguments inclusively.This will return an empty byte if an empty file, or any error, is encountered.


Function WalkGoFiles

  • func WalkGoFiles(path string) (files []string) #
  • util.go:51:61 #

WalkGoFiles recursively moves through the directory tree specified by pathproviding a slice of files matching the *.go extention. Explicitlyspecifying a file will return that file.


Function AdjustSource

  • func AdjustSource(source string, adjustBraces bool) string #
  • util.go:66:77 #

AdjustSource is a convenience function that strips the opening and closingbraces of a function's ( or other things ) body and removes the first \tcharacter on each remaining line.


Function Visit

  • func (w Walker) Visit(node ast.Node) ast.Visitor #
  • util.go:83:88 #

Visit steps through each node within the specified syntax tree.


File value.go

Type Value

  • type Value struct #
  • value.go:9:16 #

Exported Fields:

  1. Kind: Describes the current value's type, eg; CONST or VAR. #
  2. Name: The name of the value. #
  3. Line: The line number within the associated source file in which this value was originally defined. #
  4. Body: The full content of the associated statement. #

Function NewValue

  • func NewValue(id *ast.Ident, parent *File) *Value #
  • value.go:19:26 #

NewValue returns a Value instance.


Function Parse

  • func (v *Value) Parse() *Value #
  • value.go:30:35 #

Parse is responsible for browsing through f.astIdent and f.tokenSet topopulate the current value's fields. ( Chainable )


Function String

  • func (v *Value) String() string #
  • value.go:38:40 #

String implements the Stringer struct and returns the current package's name.


License

Copyright © 2022 Wilhelm Murdoch.

This project is MIT licensed.

About

Gadget is a tool that allows you to quickly inspect your Go source code. It's effectively a small layer of abstraction built on top of the Go AST package.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project