jsonenums/jsonenums.go
2015-01-29 11:35:15 +01:00

290 lines
8.4 KiB
Go

// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// JSONenums is a tool to automate the creation of methods that satisfy the
// fmt.Stringer, json.Marshaler and json.Unmarshaler interfaces.
// Given the name of a (signed or unsigned) integer type T that has constants
// defined, stringer will create a new self-contained Go source file implementing
//
// func (t T) String() string
// func (t T) MarshalJSON() ([]byte, error)
// func (t *T) UnmarshalJSON([]byte) error
//
// The file is created in the same package and directory as the package that defines T.
// It has helpful defaults designed for use with go generate.
//
// JSONenums is a simple implementation of a concept and the code might not be
// the most performant or beautiful to read.
//
// For example, given this snippet,
//
// package painkiller
//
// type Pill int
//
// const (
// Placebo Pill = iota
// Aspirin
// Ibuprofen
// Paracetamol
// Acetaminophen = Paracetamol
// )
//
// running this command
//
// jsonenums -type=Pill
//
// in the same directory will create the file pill_jsonenums.go, in package painkiller,
// containing a definition of
//
// func (r Pill) String() string
// func (r Pill) MarshalJSON() ([]byte, error)
// func (r *Pill) UnmarshalJSON([]byte) error
//
// That method will translate the value of a Pill constant to the string representation
// of the respective constant name, so that the call fmt.Print(painkiller.Aspirin) will
// print the string "Aspirin".
//
// Typically this process would be run using go generate, like this:
//
// //go:generate stringer -type=Pill
//
// If multiple constants have the same value, the lexically first matching name will
// be used (in the example, Acetaminophen will print as "Paracetamol").
//
// With no arguments, it processes the package in the current directory.
// Otherwise, the arguments must name a single directory holding a Go package
// or a set of Go source files that represent a single Go package.
//
// The -type flag accepts a comma-separated list of types so a single run can
// generate methods for multiple types. The default output file is t_string.go,
// where t is the lower-cased name of the first type listed. THe suffix can be
// overridden with the -suffix flag.
//
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"go/format"
"go/parser"
"go/token"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"golang.org/x/tools/go/exact"
"golang.org/x/tools/go/types"
_ "golang.org/x/tools/go/gcimporter"
)
var (
typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
outputSuffix = flag.String("suffix", "_jsonenums", "suffix to be added to the output file")
)
func main() {
flag.Parse()
if len(*typeNames) == 0 {
log.Fatalf("the flag -type must be set")
}
types := strings.Split(*typeNames, ",")
// Only one directory at a time can be processed, and the default is ".".
dir := "."
if args := flag.Args(); len(args) == 1 {
dir = args[0]
} else if len(args) > 1 {
log.Fatalf("only one directory at a time")
}
pkg, err := parsePackage(dir, *outputSuffix+".go")
if err != nil {
log.Fatalf("parsing package: %v", err)
}
var analysis = struct {
Command string
PackageName string
TypesAndValues map[string][]string
}{
Command: strings.Join(os.Args[1:], " "),
PackageName: pkg.name,
TypesAndValues: make(map[string][]string),
}
// Run generate for each type.
for _, typeName := range types {
values, err := pkg.valuesOfType(typeName)
if err != nil {
log.Fatalf("finding values for type %v: %v", typeName, err)
}
analysis.TypesAndValues[typeName] = values
var buf bytes.Buffer
if err := generatedTmpl.Execute(&buf, analysis); err != nil {
log.Fatalf("generating code: %v", err)
}
src, err := format.Source(buf.Bytes())
if err != nil {
// Should never happen, but can arise when developing this code.
// The user can compile the output to see the error.
log.Printf("warning: internal error: invalid Go generated: %s", err)
log.Printf("warning: compile the package to analyze the error")
src = buf.Bytes()
}
output := strings.ToLower(typeName + *outputSuffix + ".go")
outputPath := filepath.Join(dir, output)
if err := ioutil.WriteFile(outputPath, src, 0644); err != nil {
log.Fatalf("writing output: %s", err)
}
}
}
type Package struct {
name string
files []*ast.File
defs map[*ast.Ident]types.Object
types *types.Package
}
// parsePackage parses the package in the given directory and returns it.
func parsePackage(directory string, skipSuffix string) (*Package, error) {
pkgDir, err := build.Default.ImportDir(directory, 0)
if err != nil {
return nil, fmt.Errorf("cannot process directory %s: %s", directory, err)
}
var files []*ast.File
fs := token.NewFileSet()
for _, name := range pkgDir.GoFiles {
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, skipSuffix) {
continue
}
if directory != "." {
name = filepath.Join(directory, name)
}
f, err := parser.ParseFile(fs, name, nil, 0)
if err != nil {
return nil, fmt.Errorf("parsing file %v: %v", name, err)
}
files = append(files, f)
}
if len(files) == 0 {
return nil, fmt.Errorf("%s: no buildable Go files", directory)
}
// type-check the package
defs := make(map[*ast.Ident]types.Object)
config := types.Config{FakeImportC: true}
info := &types.Info{Defs: defs}
types, err := config.Check(directory, fs, files, info)
if err != nil {
return nil, fmt.Errorf("type-checking package: %v", err)
}
return &Package{
name: files[0].Name.Name,
files: files,
defs: defs,
types: types,
}, nil
}
// generate produces the String method for the named type.
func (pkg *Package) valuesOfType(typeName string) ([]string, error) {
var values, inspectErrs []string
for _, file := range pkg.files {
ast.Inspect(file, func(node ast.Node) bool {
decl, ok := node.(*ast.GenDecl)
if !ok || decl.Tok != token.CONST {
// We only care about const declarations.
return true
}
if vs, err := pkg.valuesOfTypeIn(typeName, decl); err != nil {
inspectErrs = append(inspectErrs, err.Error())
} else {
values = append(values, vs...)
}
return false
})
}
if len(inspectErrs) > 0 {
return nil, fmt.Errorf("inspecting code:\n\t%v", strings.Join(inspectErrs, "\n\t"))
}
if len(values) == 0 {
return nil, fmt.Errorf("no values defined for type %s", typeName)
}
return values, nil
}
func (pkg *Package) valuesOfTypeIn(typeName string, decl *ast.GenDecl) ([]string, error) {
var values []string
// The name of the type of the constants we are declaring.
// Can change if this is a multi-element declaration.
typ := ""
// Loop over the elements of the declaration. Each element is a ValueSpec:
// a list of names possibly followed by a type, possibly followed by values.
// If the type and value are both missing, we carry down the type (and value,
// but the "go/types" package takes care of that).
for _, spec := range decl.Specs {
vspec := spec.(*ast.ValueSpec) // Guaranteed to succeed as this is CONST.
if vspec.Type == nil && len(vspec.Values) > 0 {
// "X = 1". With no type but a value, the constant is untyped.
// Skip this vspec and reset the remembered type.
typ = ""
continue
}
if vspec.Type != nil {
// "X T". We have a type. Remember it.
ident, ok := vspec.Type.(*ast.Ident)
if !ok {
continue
}
typ = ident.Name
}
if typ != typeName {
// This is not the type we're looking for.
continue
}
// We now have a list of names (from one line of source code) all being
// declared with the desired type.
// Grab their names and actual values and store them in f.values.
for _, name := range vspec.Names {
if name.Name == "_" {
continue
}
// This dance lets the type checker find the values for us. It's a
// bit tricky: look up the object declared by the name, find its
// types.Const, and extract its value.
obj, ok := pkg.defs[name]
if !ok {
return nil, fmt.Errorf("no value for constant %s", name)
}
info := obj.Type().Underlying().(*types.Basic).Info()
if info&types.IsInteger == 0 {
return nil, fmt.Errorf("can't handle non-integer constant type %s", typ)
}
value := obj.(*types.Const).Val() // Guaranteed to succeed as this is CONST.
if value.Kind() != exact.Int {
log.Fatalf("can't happen: constant is not an integer %s", name)
}
values = append(values, name.Name)
}
}
return values, nil
}