Skip to content

Repository files navigation

Golang Gentools

GitHub license Workflow Status GoReportCard example GoDoc reference example GitHub release GitHub Pages

Gopher

Package gentools contains Golang tools used for scanning and parsing golang code. It is based on the golang ast and provides simple but very powerful functionality in terms of code scanning.

The API is expected to be simpler and less verbose than go/types package, that looks more like reflect.

Parsing packages

This package allows reading and scanning the content of go files based on the non-runtime AST definitions.

The types.PackageMap contains context of mapped packages with their structs and dependencies.

The map could get loaded by using LoadPackages function or types.PackageMap method with the same name. Both of these loads provided input packages, whereas the method gets only required that doesn't already exist in itself.

Generic declarations

Gentools preserves the existing concrete types.Struct, types.Interface, types.Alias, and types.Function values for generic declarations. Their additive GenericInfo() accessor exposes declared and receiver type parameters; each parameter includes its owner, ordinal, and constraint. A use such as Box[string] is represented by *types.Instantiation, whose Origin and ordered Arguments distinguish it from Box[int] and the uninstantiated declaration. Generic aliases are supported by the selected Go 1.27.1 toolchain and require GODEBUG=gotypesalias=1 while loading packages.

Development

The project toolkit is pinned to Go 1.27.1 through .go-version. Make targets use go from PATH and fail unless it reports the pinned version. Configure Go through your IDE or a version manager such as mise or asdf:

make test

The canonical lint gate uses golangci-lint v2.13.2. Install that exact release from the official binary installer into a directory on PATH, then run:

curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v2.13.2
golangci-lint version
golangci-lint config verify --config .golangci.yml
make golangci-lint

The local command and hosted lint job both apply .golangci.yml to the root module (./...). Findings produce a non-zero result. See the CI and lint quickstart for failure-path and matrix validation evidence.

Test quality and examples

Visit the Gentools guides for a polished, browser-friendly entry point to the runnable example and testing guidance.

Run the complete quality workflow with the selected toolchain:

make test-quality

The consumer scenarios distinguish candidate-source validation from the tag-triggered published-module smoke test. See the integration guide and unit-test gap assessment.

For a runnable code-generation example, create a deterministic struct summary:

go run ./examples/codegen/struct-summary \
  -input ./examples/codegen/struct-summary/testdata \
  -output /tmp/summary.go

To generate typed getters for only the fields you select, run:

go run ./examples/codegen/struct-accessors \
  -input ./examples/codegen/struct-accessors/testdata \
  -type Account \
  -fields ID,Email \
  -output /tmp/account_accessors.go

For an annotated API contract example, run:

go run ./examples/codegen/openapi-contract \
  -input ./examples/codegen/openapi-contract/testdata \
  -output /tmp/gentools-openapi.json

Operations use comments such as @openapi method=GET route=/pets/{id} summary="Get a pet" response=200:Pet. The example supports documented primitive, pointer, slice, and nested struct fields, reuses shared model definitions, and fails with a source declaration diagnostic for malformed, duplicate, unresolved, or unsupported input. See the OpenAPI contract quickstart and the code-generation guide.

GitHub Pages publication is configured by .github/workflows/pages.yml; a repository administrator must select GitHub Actions as the Pages source as described in docs/publishing.md.

Go 1.27.1 migration and release policy

This feature drops support for Go releases before 1.27.1. Upgrade the Go toolchain selected by your version manager or IDE to the version in .go-version, then run the validation commands in the feature quickstart.

A release containing this support-boundary change is breaking. Before publishing it, release it under the next major-version policy and include this migration guidance in the release notes, as required by the project constitution.

Example:

package main

import (
	"fmt"
	"os"

	"github.com/kucjac/gentools/types"
	"github.com/kucjac/gentools/parser"
)

func main() {
	pkgs, err := parser.LoadPackages(parser.LoadConfig{
		// Paths should contain file system paths to related golang directories.			
		// Paths: []string{"/home/user/golang/src/github.com/kucjac/gentools", "./../mypackage"},
		
		// PkgNames should contain full package names to get. It could be set along with the 'Paths' field.
		PkgNames:   []string{"github.com/kucjac/gentools"},
		BuildFlags: nil,
		Verbose:    false,
	})
	if err != nil {
		fmt.Printf("Err: Loading packages failed: %s\n", err)
		os.Exit(1)
	}
}

Extracting declarations

Based on loaded packages a developer can operate on the types loaded along with these packages.

// Let's get the types.Type interface and check it's content.
t, ok := pkgs.TypeOf("types.Type", nil)
if !ok {
    fmt.Println("Err: getting types.Type failed")
    os.Exit(1)
}

// We can get the name of given type, with or without it's identifier.
fmt.Println(t.Name(true, "")) // types.Type
fmt.Println(t.Name(false, "")) // Type

// We can set up current working package context while getting the name. 
// This way the result should not contain the identifier.
// The package context could be an identifier or even better full package name.
fmt.Println(t.Name(true, "types")) // Type

While the packages got loaded not only selected packages were provided but also all dependency imports.

mutexType, ok := pkgs.TypeOf("*sync.Mutex", nil)
if !ok {
	fmt.Println("Err: getting *sync.Mutex failed")
	os.Exit(1)
}

fmt.Println(mutexType.Name(true, "")) // *sync.Mutex
// We can dereference the pointer, slice, array, channel or wrapper by using 'Elem' method. 
mutexType = mutexType.Elem()
fmt.Println(mutexType.Name(true, "")) // sync.Mutex

The types allows to easily create and operate on given types with very simple API.

structType, ok := pkgs.TypeOf("types.Struct")
if !ok {
	fmt.Println("Err: getting types.Struct failed")
	os.Exit(1)
}

// Let's create a chan of slices of pointers to the given Struct.
newType := types.ChanOf(types.RecvOnly, types.SliceOf(types.PointerTo(structType)))
fmt.Println(sliceType) // chan <-[]*types.Struct

The package reads all methods and structure fields for loaded types along with the metadata like receiver type (methods) and name, or parameter names.

sliceType, ok := pkgs.TypeOf("types.Array")
if !ok {
	fmt.Println("Err: getting types.Struct failed")
	os.Exit(1)
}

st := sliceType.(*types.Struct)
for i, sField := range st.Fields {
	fmt.Printf("Field: %d - %s\t%s\n", i, sField.Name, sField.Type)
}

for i, method := range st.Methods {
	fmt.Printf("Method: %d - %s\n", i, method)
}

Each package could also be extracted out of all packages. It is also possible to extract any declaration out of the specific package in a way like:

const typesPkgName = "github.com/kucjac/gentools/types"
typesPkg, ok := pkgs.PackageByPath(typesPkgName)
if !ok {
	fmt.Printf("Err: types package: %s not parsed\n", typesPkgName)
	os.Exit(1)
}

fmt.Println(typesPkg.GetPkgPath()) // github.com/kucjac/gentools/types
fmt.Println(typesPkg.Identifier) // types

parsedPkg, ok := typesPkg.GetType("Package") // This gets parsed types.Package type.
if !ok {
	fmt.Println("Err: package types can't get the type 'Package'")
	os.Exit(1)
}

Comparing types

Each type could be compared to the other in two ways:

Compare equality:

Two types could be compared on their equality by calling the Equal method of one type and providing another as an argument.

If two types are exactly the same then the value would result as true.

i.e.:

// Check if the type String is equal to the type Bool 
fmt.Println(types.String.Equal(types.Bool)) // false

// But exactly the same types would give a positive result i.e:
fmt.Println(types.String.Equal(types.String)) // true

// This also applies to any other type - slice, struct, alias, interface, map, pointer etc i.e.:
fmt.Println(types.SliceOf(types.String).Equal(types.SliceOf(types.String))) // true

It is important to see that the aliases would not be equal to the type they point to i.e.:

type AliasOfInt int

// Load this package and compare types ...

fmt.Println(aliasOfIntType.Equal(types.Int)) // false

// But if we try to derefence this alias the value would be positive:

fmt.Println(aliasOfIntType.Elem().Equal(types.Int)) // true

Compare by Kind

In some situations if we don't want to check if the types are equal directly, we can compare the kind of the type.

As an example, we might want to check if the type that we're looking for is a pointer, and if we need to dereference it.

All dereferences are done by using Elem() method, which panics if there is nothing to dereference - you cannot dereference basic types, structs etc.

These are supported type kinds:

  • KindBool
  • KindInt
  • KindInt8
  • KindInt16
  • KindInt32
  • KindInt64
  • KindUint
  • KindUint8
  • KindUint16
  • KindUint32
  • KindUint64
  • KindUintptr
  • KindFloat32
  • KindFloat64
  • KindComplex64
  • KindComplex128
  • KindString
  • KindArray
  • KindChan
  • KindFunc
  • KindInterface
  • KindMap
  • KindPtr
  • KindSlice
  • KindStruct
  • KindUnsafePointer

In order to compare some types kinds simply use the Kind method of the type and compare it like:

fmt.Println(type1.Kind() == type2.Kind()) // KindBool == KindBool = true 

This might be super helpful in terms of comparing the kind of aliased types. I.e. if we have alias type mentioned above:

// AliasOfInt is a type wrapper over the `int` standard type.
type AliasOfInt int

// As previously known this would fail
fmt.Println(aliasOfIntType.Equal(types.Int)) // false

// But we can compare it's kind which would result
fmt.Println(aliasOfIntType.Kind() == types.KindInt) // true

Interface comparison

This package supports also interfaces comparison. The parser deeply checks all methods and provides a way to check if an type (struct, interface) implements also another interface.

i.e.:

osFile, ok := pkgs.TypeOf("*os.File")
if !ok {
	fmt.Println("*os.File not found")
	os.Exit(1)
}


ioReader, ok := pkgs.TypeOf("io.Reader")
if !ok {
	fmt.Println("io.Reader not found")
	os.Exit(1)
}

// The function types.Implements allows, checking if a type implements provided interface. 
fmt.Println(types.Implements(osFile, io.Reader.(*types.Interface))) // true

ioReadCloser, ok := pkgs.TypeOf("io.ReadCloser")
if !ok {
	fmt.Println("io.ReadCloser not found")
	os.Exit(1)
}

// It could also be an interface - io.ReadCloser implements io.Reader interface.
fmt.Println(types.Implements(ioReadCloser, io.Reader.(*types.Interface))) // true

About

Package astreflect contains Golang ast based types reflection.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages