*GoParser API
The library main type is the *GoParser
. With this struct
, you can call methods that iterate
over the go
parsed code, allowing you to gather information and generate new code (be it go
, js
,
ts
, etc).
This type is a wrapper around the golang.org/x/tools/go/packages
API, that at the time I used
was very hard to understand and use. After reading this article
multiple times, I came with this idea.
danger
I strongly recommend you to read the above article, to fully understand what we'll talk about. I'll assume that you've read
#
Creating a *GoParser instanceYou can create a new *GoParser
instance using the NewGoParser
function, exported by the root package.
It takes a pattern
string
and a Config
struct
.
func NewGoParser(pattern string, config Config) (*GoParser, error) { ... }
The pattern
string
is forwarded directly to the packages.Load
function. If you want to see details
about what is this string, go to the official packages docs.
info
When you create the *GoParser
instance (calling the NewGoParser
function), the packages.Load
function is called
to parse the code. Depending on the size of the code to parse, this function call can be expensive.
note
Any errors that occur when calling packages.Load
will be directly returned to the caller of NewGoParser
,
with a nil *GoParser
reference.
#
ConfigurationBefore creating a new *GoParser
instance, you will need to read about the Config
struct, that
acts as a wrapper around the packages.Config
, plus some other info:
type Config struct { Tests bool Dir string Env []string Fset *token.FileSet BuildFlags []string Focus *Focus Logger LoggerCLI LogFlags uint64}
The Tests
, Dir
, Env
, Fset
and BuildFlags
are directly forwarded to the underlying packages.Config
struct. If you need to see more info about it, see the packages.Config docs
note
You will notice that the packages.Config
has many fields that aren't present inside the go-codegen.Config
.
This is on purpose. If you need to use one of the excluded fields, please let me know
The Logger
field is used to allow the library user to pass an already created logger instance, instead of relying on
the one created by the library itself. This is extremely useful, since this library will usually be used to compose
other libraries, the logger instance usually already exists.
The LogFlags
field is used to control the amount of information that the library will write to the stdout
,
using the LoggerCLI (another library that belongs to
my personal stack). The flags will be directly forwarded to it. Note that since this lib uses the LoggerCLI
, it's
possible to use the beautify
package, from the LoggerCLI
itself, to pretty-print the generated output. You can see
the available flags at the go-log
official documentation (note that the go-codegen
library re-exports these flags, so you don't need to point to go-log
directly).
tip
In addition to the original go-log
flags, there's an extra one: LogJSON
. This extra flag belongs to the go-codegen
itself, and is used to control whether the logs are converted (or not) to json
, before being sent to the stdout
note
If you set the value of the Logger
field, the LogFlags
field will be completely ignored, since it is only used when
the library needs to dynamically create a new LoggerCLI
instance
#
FocusSometimes, you will want to parse the go
code but iterate only over some specific thing. You can parse a
package that contains many files, but just want to iterate over a specific file, for example. To do it, you
must give a *Focus
to the Config
, at *GoParser
creation time.
The root
package exports three functions that you can use to create a new *Focus
:
func FocusPackagePath(packagePath string) *Focus { ... }func FocusFilePath(filePath string) *Focus { ... }func FocusTypeName(typeName string) *Focus { ... }
caution
It's very important to note that the string received by the above functions is considered a RegExp
. It's not a simple
string. It means that if you pass the "Input"
string
to the FocusTypeName
function, the following typenames
will match:
InputStruct
, Input
, MyInputStruct
, etc
If you want an exact match, use the ^
and $
characters, that set the start/end of the string, respectively: "^Input$"
. It is just
an example, but you can use all the features that you would use inside a regexp.MatchString()
method call
Example: if you want to parse the codeToParse.go
file, that contains 3 structs
, but iterate only over the
StructB
, you can do something like this (consider workdir
to be anything):
package main
type StructA struct { FieldA string FieldB string}
type StructB struct { FieldC string FieldD string}
type StructC struct { FieldE string FieldF string}
package main
import "github.com/mathbalduino/go-codegen"
func main() { config := parser.Config{ Focus: parser.FocusTypeName("StructB"), } // ...}
With this Config
, the iterator will skip every type name that's different from "StructB"
.
You can combine more than one *Focus
using the MergeFocus
function, exported by the root package:
func MergeFocus(f1 *Focus, f2 *Focus) *Focus { ... }
This function will copy the first argument to a third *Focus
and override it with the values of the second one. This
way, you can have a *Focus
that filters by typename
and package
, simultaneously, for example.
#
Iterate interfacesAfter the *GoParser
instantiation (and code parsing), you can call the method below to iterate over interfaces
:
// just an aliastype InterfacesIterator = func(interface_ *types.TypeName, logger LoggerCLI) error
func (p *GoParser) IterateInterfaces(callback InterfacesIterator, optionalLogger ...LoggerCLI) error { ... }
note
Note that you have the option to give a new LoggerCLI
instance, at the moment you call IterateInterfaces
. It's very
useful when you're using the beautify
package, from go-log, and want to customize
the log display
You can completely omit it (since it is a variadic), in which case the logger of the *GoParser
instance will be used instead
With this method, you pass a callback function that will be executed once for every interface
type inside the
parsed code:
package main
type StructA struct { FieldA string FieldB string}
type InterfaceA interface { MethodA() MethodB()}
// privatetype interfaceB interface { methodC() methodD()}
package main
import ( "fmt" "github.com/mathbalduino/go-codegen" "go/types")
func main() { config := parser.Config{} // Assuming the code is being executed at <workdir> p, e := parser.NewGoParser("./", config) if e != nil { panic(e) } // stdout: // InterfaceA // interfaceB e = p.IterateInterfaces(func(interface_ *types.TypeName, logger parser.LoggerCLI) error { fmt.Println(interface_.Name()) return nil }) if e != nil { panic(e) }}
note
The example above will parse the <workdir>/main.go
too, but it will be completely ignored, since there's no
interfaces
inside it
note
If you return some error from the callback function, this error will be returned to the caller of
IterateInterfaces
, stopping its execution.
#
Iterate structsAfter the *GoParser
instantiation (and code parsing), you can call the method below to iterate over structs
:
// just an aliastype StructsIterator = func(struct_ *types.TypeName, logger LoggerCLI) error
func (p *GoParser) IterateStructs(callback StructsIterator, optionalLogger ...LoggerCLI) error { ... }
note
Note that you have the option to give a new LoggerCLI
instance, at the moment you call IterateInterfaces
. It's very
useful when you're using the beautify
package, from go-log, and want to customize
the log display
You can completely omit it (since it is a variadic), in which case the logger of the *GoParser
instance will be used instead
With this method, you pass a callback function that will be executed once for every struct
type inside the
parsed code:
package main
type StructA struct { FieldA string FieldB string}
// privatetype structB interface { fieldA string fieldB string}
type interfaceB interface { methodC() methodD()}
package main
import ( "fmt" "github.com/mathbalduino/go-codegen" "go/types")
func main() { config := parser.Config{} // Assuming the code is being executed at <workdir> p, e := parser.NewGoParser("./", config) if e != nil { panic(e) } // stdout: // StructA // structB e = p.IterateStructs(func(struct_ *types.TypeName, logger parser.LoggerCLI) error { fmt.Println(struct_.Name()) return nil }) if e != nil { panic(e) }}
note
The example above will parse the <workdir>/main.go
too, but it will be completely ignored, since there's no
structs
inside it
note
If you return some error from the callback function, this error will be returned to the caller of
IterateStructs
, stopping its execution.
#
Other iteratorsThe library comes with other iterators, but I keep them private to reduce the API size. Take a look:
func (p *GoParser) iterateTypeNames(callback typeNamesIterator) error { ... }func (p *GoParser) iterateFiles(callback filesIterator) error { ... }func (p *GoParser) iteratePackages(callback packagesIterator) error { ... }
Internally, these methods are used to compose the exported ones.
note
If you think it's interesting to export any of these methods, let me know
#
Some notesAs you've seemed, this library doesn't abstract the most primitive types from go/types
. When you call IterateInterfaces
or IterateStructs
, the *types.TypeName
that the callback receives has all the necessary information about the
underlying type (name, fields, methods, etc).
The library iterates over the parsed code using a single-threaded
strategy. This can be improved in the future, but for
now I don't see the necessity. If you think I'm wrong, please let me know.