input/elasticapm/internal/modeldecoder/generator/parser.go (156 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package generator
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/pkg/errors"
"golang.org/x/tools/go/packages"
)
// Parsed contains information about a parsed package,
// including the package name, type information
// and some pre-defined variables
type Parsed struct {
// Dir holds the filesystem directory holding the parsed Go package.
Dir string
// package name
pkgName string
// parsed structs from loading types from the provided package
structTypes map[string]structType
// parsed pattern variables
patternVariables map[string]string
// parsed enumeration values
enumVariables map[string][]string
}
type structType struct {
name string
comment string
fields []structField
}
type structField struct {
*types.Var
tag reflect.StructTag
comment string
}
// Parse loads the Go package named by the given package pattern
// and creates a parsed struct containing the package name,
// type information and some pre-defined variables
// It returns an error if the package cannot be successfully loaded
// or parsed
func Parse(pkgPattern string) (*Parsed, error) {
pkg, err := loadPackage(pkgPattern)
if err != nil {
return nil, err
}
parsed := Parsed{
structTypes: make(map[string]structType),
patternVariables: make(map[string]string),
enumVariables: make(map[string][]string),
pkgName: pkg.Types.Name(),
Dir: filepath.Dir(pkg.GoFiles[0]),
}
err = parse(pkg, &parsed)
return &parsed, err
}
func loadPackage(pkg string) (*packages.Package, error) {
cfg := packages.Config{
Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedImports | packages.NeedFiles,
}
pkgs, err := packages.Load(&cfg, pkg)
if err != nil {
return nil, err
}
if packages.PrintErrors(pkgs) > 0 {
return nil, errors.New("packages load error")
}
return pkgs[0], nil
}
func parse(pkg *packages.Package, parsed *Parsed) error {
for _, file := range pkg.Syntax {
// parse type comments
typeComments := make(map[int]string)
for _, c := range file.Comments {
typeComments[int(c.End())] = trimComment(c.Text())
}
for _, decl := range file.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
switch genDecl.Tok {
case token.VAR:
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for i, expr := range valueSpec.Values {
name := valueSpec.Names[i].Name
var err error
switch v := expr.(type) {
case *ast.BasicLit:
if strings.HasPrefix(name, "pattern") {
parsed.patternVariables[name], err = strconv.Unquote(v.Value)
}
case *ast.CompositeLit:
if strings.HasPrefix(name, "enum") {
elems := make([]string, len(v.Elts))
for i := 0; i < len(v.Elts); i++ {
elems[i], err = strconv.Unquote(v.Elts[i].(*ast.BasicLit).Value)
}
parsed.enumVariables[name] = elems
}
}
if err != nil {
return err
}
}
}
case token.TYPE:
// find comments for the generic declaration node, in the format of
// //MyComment
// type MyComment struct {..}
var genDeclComment string
if c, ok := typeComments[int(genDecl.Pos()-1)]; ok {
genDeclComment = c
}
// iterate through the type declaration for this generic declaration node
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
obj := pkg.TypesInfo.Defs[typeSpec.Name]
if obj == nil {
continue
}
var st structType
st.name = obj.Name()
// find comments for the specific type declaration, in the format of
// type (
// //MyComment
// MyComment struct {..}
// )
// fallback to generic declaration comment otherwise if it starts with the type name
if c, ok := typeComments[int(genDecl.Pos()-1)]; ok && st.name == strings.Split(c, " ")[0] {
st.comment = c
} else if st.name == strings.Split(genDeclComment, " ")[0] {
st.comment = genDeclComment
}
// find field comments (ignoring line comments)
fieldComments := make(map[string]string)
if st, ok := typeSpec.Type.(*ast.StructType); ok {
for _, f := range st.Fields.List {
if f.Doc != nil && len(f.Doc.Text()) > 0 && len(f.Names) > 0 {
fieldComments[f.Names[0].Name] = trimComment(f.Doc.Text())
}
}
}
named := obj.(*types.TypeName).Type().(*types.Named)
typesStruct, ok := named.Underlying().(*types.Struct)
if !ok {
return fmt.Errorf("unhandled type %T", named.Underlying())
}
numFields := typesStruct.NumFields()
structFields := make([]structField, 0, numFields)
for i := 0; i < numFields; i++ {
structField := structField{
Var: typesStruct.Field(i),
tag: reflect.StructTag(typesStruct.Tag(i)),
}
if c, ok := fieldComments[structField.Name()]; ok {
structField.comment = c
}
structFields = append(structFields, structField)
}
st.fields = structFields
parsed.structTypes[obj.Type().String()] = st
}
}
}
}
return nil
}
func trimComment(c string) string {
c = strings.ReplaceAll(c, "\n", " ")
return strings.TrimSuffix(c, " ")
}