processor/processor.go (483 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 processor
import (
"fmt"
"go/ast"
"go/token"
gotypes "go/types"
"regexp"
"sort"
"strings"
"github.com/elastic/crd-ref-docs/config"
"github.com/elastic/crd-ref-docs/types"
"go.uber.org/zap"
"golang.org/x/tools/go/packages"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-tools/pkg/crd"
crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
"sigs.k8s.io/controller-tools/pkg/loader"
"sigs.k8s.io/controller-tools/pkg/markers"
)
const (
objectRootMarker = "kubebuilder:object:root"
)
var ignoredCommentRegex = regexp.MustCompile(`\s*^(?i:\+|copyright)`)
type groupVersionInfo struct {
schema.GroupVersion
*loader.Package
doc string
kinds map[string]struct{}
types types.TypeMap
markers markers.MarkerValues
}
func Process(config *config.Config) ([]types.GroupVersionDetails, error) {
compiledConfig, err := compileConfig(config)
if err != nil {
return nil, err
}
p, err := newProcessor(compiledConfig, config.Flags.MaxDepth)
if err != nil {
return nil, err
}
// locate the packages annotated with group names
if err := p.findAPITypes(config.SourcePath); err != nil {
return nil, fmt.Errorf("failed to find API types in directory %s:%w", config.SourcePath, err)
}
p.types.InlineTypes(p.propagateReference)
p.types.PropagateMarkers()
p.parseMarkers()
// collect references between types
for typeName, refs := range p.references {
typeDef, ok := p.types[typeName]
if !ok {
return nil, fmt.Errorf("type not loaded: %s", typeName)
}
for ref, _ := range refs {
if rd, ok := p.types[ref]; ok {
typeDef.References = append(typeDef.References, rd)
}
}
}
// build the return array
var gvDetails []types.GroupVersionDetails
for _, gvi := range p.groupVersions {
details := types.GroupVersionDetails{GroupVersion: gvi.GroupVersion, Doc: gvi.doc}
for k, _ := range gvi.kinds {
details.Kinds = append(details.Kinds, k)
}
details.Types = make(types.TypeMap)
for name, t := range gvi.types {
key := types.Identifier(t)
if p.shouldIgnoreType(key) {
zap.S().Debugw("Skipping excluded type", "type", name)
continue
}
if typeDef, ok := p.types[key]; ok && typeDef != nil {
details.Types[name] = typeDef
} else {
zap.S().Fatalw("Type not loaded", "type", key)
}
}
details.Markers = gvi.markers
gvDetails = append(gvDetails, details)
}
// sort the array by GV
sort.SliceStable(gvDetails, func(i, j int) bool {
if gvDetails[i].Group < gvDetails[j].Group {
return true
}
if gvDetails[i].Group == gvDetails[j].Group {
return gvDetails[i].Version < gvDetails[j].Version
}
return false
})
return gvDetails, nil
}
func newProcessor(compiledConfig *compiledConfig, maxDepth int) (*processor, error) {
registry, err := mkRegistry(compiledConfig.markers)
if err != nil {
return nil, err
}
p := &processor{
compiledConfig: compiledConfig,
maxDepth: maxDepth,
parser: &crd.Parser{
Collector: &markers.Collector{Registry: registry},
Checker: &loader.TypeChecker{},
},
groupVersions: make(map[schema.GroupVersion]*groupVersionInfo),
types: make(types.TypeMap),
references: make(map[string]map[string]struct{}),
}
crd.AddKnownTypes(p.parser)
return p, err
}
type processor struct {
*compiledConfig
maxDepth int
parser *crd.Parser
groupVersions map[schema.GroupVersion]*groupVersionInfo
types types.TypeMap
references map[string]map[string]struct{}
}
func (p *processor) findAPITypes(directory string) error {
cfg := &packages.Config{Dir: directory}
pkgs, err := loader.LoadRootsWithConfig(cfg, "./...")
if err != nil {
return err
}
for _, pkg := range pkgs {
gvInfo := p.extractGroupVersionIfExists(p.parser.Collector, pkg)
if gvInfo == nil {
continue
}
if p.shouldIgnoreGroupVersion(gvInfo.GroupVersion.String()) {
continue
}
// let the parser know that we need this package
p.parser.AddPackage(pkg)
// if we have encountered this GV before, use that instead
if gv, ok := p.groupVersions[gvInfo.GroupVersion]; ok {
gvInfo = gv
} else {
p.groupVersions[gvInfo.GroupVersion] = gvInfo
}
if gvInfo.types == nil {
gvInfo.types = make(types.TypeMap)
}
// locate the kinds
markers.EachType(p.parser.Collector, pkg, func(info *markers.TypeInfo) {
// ignore types explicitly listed by the user
if p.shouldIgnoreType(fmt.Sprintf("%s.%s", pkg.PkgPath, info.Name)) {
return
}
// ignore unexported types
if info.RawSpec.Name == nil || !info.RawSpec.Name.IsExported() {
return
}
// load the type
key := fmt.Sprintf("%s.%s", pkg.PkgPath, info.Name)
typeDef, ok := p.types[key]
if !ok {
typeDef = p.processType(pkg, nil, pkg.TypesInfo.TypeOf(info.RawSpec.Name), 0)
}
if typeDef != nil && typeDef.Kind != types.BasicKind {
gvInfo.types[info.Name] = typeDef
}
// is this a root object?
if root := info.Markers.Get(objectRootMarker); root != nil {
if gvInfo.kinds == nil {
gvInfo.kinds = make(map[string]struct{})
}
gvInfo.kinds[info.Name] = struct{}{}
typeDef.GVK = &schema.GroupVersionKind{Group: gvInfo.Group, Version: gvInfo.Version, Kind: info.Name}
}
})
}
return nil
}
func (p *processor) extractGroupVersionIfExists(collector *markers.Collector, pkg *loader.Package) *groupVersionInfo {
markerValues, err := markers.PackageMarkers(collector, pkg)
if err != nil {
pkg.AddError(err)
return nil
}
groupName := markerValues.Get("groupName")
if groupName == nil {
return nil
}
version := pkg.Name
if v := markerValues.Get("versionName"); v != nil {
version = v.(string)
}
gvInfo := &groupVersionInfo{
GroupVersion: schema.GroupVersion{
Group: groupName.(string),
Version: version,
},
Package: pkg,
doc: p.extractPkgDocumentation(pkg),
markers: markerValues,
}
return gvInfo
}
func (p *processor) extractPkgDocumentation(pkg *loader.Package) string {
var pkgComments []string
pkg.NeedSyntax()
for _, n := range pkg.Syntax {
if n.Doc == nil {
continue
}
comment := n.Doc.Text()
commentLines := strings.Split(comment, "\n")
for _, line := range commentLines {
if !ignoredCommentRegex.MatchString(line) {
pkgComments = append(pkgComments, line)
}
}
}
return strings.Join(pkgComments, "\n")
}
func (p *processor) processType(pkg *loader.Package, parentType *types.Type, t gotypes.Type, depth int) *types.Type {
typeDef, rawType := mkType(pkg, t)
typeID := types.Identifier(typeDef)
if !rawType && p.shouldIgnoreType(typeID) {
zap.S().Debugw("Skipping excluded type", "type", typeID)
return nil
}
if processed, ok := p.types[typeDef.UID]; ok {
return processed
}
info := p.parser.LookupType(pkg, typeDef.Name)
if info != nil {
typeDef.Doc = info.Doc
typeDef.Markers = info.Markers
if p.useRawDocstring && info.RawDecl != nil {
// use raw docstring to support multi-line and indent preservation
typeDef.Doc = strings.TrimSuffix(info.RawDecl.Doc.Text(), "\n")
}
}
if depth > p.maxDepth {
zap.S().Warnw("Not loading type due to reaching max recursion depth", "type", t.String())
typeDef.Kind = types.UnknownKind
return typeDef
}
zap.S().Debugw("Load", "package", typeDef.Package, "name", typeDef.Name)
switch t := t.(type) {
case nil:
typeDef.Kind = types.UnknownKind
zap.S().Warnw("Failed to determine AST type", "package", pkg.PkgPath, "type", t.String())
case *gotypes.Named:
// Import the type's package if not within current package
if typeDef.Package != pkg.PkgPath {
imports := pkg.Imports()
importPkg, ok := imports[typeDef.Package]
if !ok {
zap.S().Warnw("Imported type cannot be found", "name", typeDef.Name, "package", typeDef.Package)
return typeDef
}
p.parser.NeedPackage(importPkg)
pkg = importPkg
}
typeDef.Kind = types.AliasKind
underlying := t.Underlying()
info := p.parser.LookupType(pkg, typeDef.Name)
if info != nil {
underlying = pkg.TypesInfo.TypeOf(info.RawSpec.Type)
}
if underlying.String() == "string" {
typeDef.EnumValues = lookupConstantValuesForAliasedType(pkg, typeDef.Name)
}
typeDef.UnderlyingType = p.processType(pkg, typeDef, underlying, depth+1)
p.addReference(typeDef, typeDef.UnderlyingType)
case *gotypes.Struct:
if parentType != nil {
// Rather than the parent being a Named type with a "raw" Struct as
// UnderlyingType, convert the parent to a Struct type.
parentType.Kind = types.StructKind
if info := p.parser.LookupType(pkg, parentType.Name); info != nil {
p.processStructFields(parentType, pkg, info, depth)
}
// Abort processing type and return nil as UnderlyingType of parent.
return nil
} else {
zap.S().Warnw("Anonymous structs are not supported", "package", pkg.PkgPath, "type", t.String())
typeDef.Name = ""
typeDef.Package = ""
typeDef.Kind = types.UnsupportedKind
}
case *gotypes.Pointer:
typeDef.Kind = types.PointerKind
typeDef.UnderlyingType = p.processType(pkg, typeDef, t.Elem(), depth+1)
if typeDef.UnderlyingType != nil {
typeDef.Package = typeDef.UnderlyingType.Package
}
case *gotypes.Slice:
typeDef.Kind = types.SliceKind
typeDef.UnderlyingType = p.processType(pkg, typeDef, t.Elem(), depth+1)
if typeDef.UnderlyingType != nil {
typeDef.Package = typeDef.UnderlyingType.Package
}
case *gotypes.Map:
typeDef.Kind = types.MapKind
typeDef.KeyType = p.processType(pkg, typeDef, t.Key(), depth+1)
typeDef.ValueType = p.processType(pkg, typeDef, t.Elem(), depth+1)
if typeDef.ValueType != nil {
typeDef.Package = typeDef.ValueType.Package
}
case *gotypes.Basic:
typeDef.Kind = types.BasicKind
typeDef.Package = ""
case *gotypes.Interface:
typeDef.Kind = types.InterfaceKind
default:
typeDef.Kind = types.UnsupportedKind
}
p.types[typeDef.UID] = typeDef
return typeDef
}
func (p *processor) processStructFields(parentType *types.Type, pkg *loader.Package, info *markers.TypeInfo, depth int) {
logger := zap.S().With("package", pkg.PkgPath, "type", parentType.String())
logger.Debugw("Processing struct fields")
parentTypeKey := types.Identifier(parentType)
for _, f := range info.Fields {
fieldDef := &types.Field{
Name: f.Name,
Markers: f.Markers,
Doc: f.Doc,
Embedded: f.Name == "",
}
if tagVal, ok := f.Tag.Lookup("json"); ok {
args := strings.Split(tagVal, ",")
if len(args) > 0 && args[0] != "" {
fieldDef.Name = args[0]
}
if len(args) > 1 && args[1] == "inline" {
fieldDef.Inlined = true
}
}
t := pkg.TypesInfo.TypeOf(f.RawField.Type)
if t == nil {
zap.S().Debugw("Failed to determine type of field", "field", fieldDef.Name)
continue
}
logger.Debugw("Loading field type", "field", fieldDef.Name)
if fieldDef.Type = p.processType(pkg, nil, t, depth); fieldDef.Type == nil {
logger.Debugw("Failed to load type for field", "field", f.Name, "type", t.String())
continue
}
// Keep old behaviour, where struct fields are never regarded as imported
fieldDef.Type.Imported = false
if fieldDef.Name == "" {
fieldDef.Name = fieldDef.Type.Name
}
if p.shouldIgnoreField(parentTypeKey, fieldDef.Name) {
zap.S().Debugw("Skipping excluded field", "type", parentType.String(), "field", fieldDef.Name)
continue
}
parentType.Fields = append(parentType.Fields, fieldDef)
p.addReference(parentType, fieldDef.Type)
}
}
func mkType(pkg *loader.Package, t gotypes.Type) (*types.Type, bool) {
qualifier := gotypes.RelativeTo(pkg.Types)
cleanTypeName := strings.TrimLeft(gotypes.TypeString(t, qualifier), "*[]")
typeDef := &types.Type{
UID: t.String(),
Name: cleanTypeName,
Package: pkg.PkgPath,
}
// Check if the type is imported
rawType := strings.HasPrefix(cleanTypeName, "struct{") || strings.HasPrefix(cleanTypeName, "interface{")
dotPos := strings.LastIndexByte(cleanTypeName, '.')
if !rawType && dotPos >= 0 {
typeDef.Name = cleanTypeName[dotPos+1:]
typeDef.Package = cleanTypeName[:dotPos]
typeDef.Imported = true
}
return typeDef, rawType
}
// Every child that has a reference to 'originalType', will also get a reference to 'additionalType'.
func (p *processor) propagateReference(originalType *types.Type, additionalType *types.Type) {
for _, parentRefs := range p.references {
if _, ok := parentRefs[originalType.UID]; ok {
parentRefs[additionalType.UID] = struct{}{}
}
}
}
func (p *processor) addReference(parent *types.Type, child *types.Type) {
if child == nil {
return
}
switch child.Kind {
case types.SliceKind, types.PointerKind:
p.addReference(parent, child.UnderlyingType)
case types.MapKind:
p.addReference(parent, child.KeyType)
p.addReference(parent, child.ValueType)
case types.AliasKind, types.StructKind:
if p.references[child.UID] == nil {
p.references[child.UID] = make(map[string]struct{})
}
p.references[child.UID][parent.UID] = struct{}{}
}
}
func mkRegistry(customMarkers []config.Marker) (*markers.Registry, error) {
registry := &markers.Registry{}
if err := registry.Define(objectRootMarker, markers.DescribesType, true); err != nil {
return nil, err
}
for _, marker := range crdmarkers.AllDefinitions {
if err := registry.Register(marker.Definition); err != nil {
return nil, err
}
}
for _, marker := range customMarkers {
t := markers.DescribesField
switch marker.Target {
case config.TargetTypePackage:
t = markers.DescribesPackage
case config.TargetTypeType:
t = markers.DescribesType
case config.TargetTypeField:
t = markers.DescribesField
default:
zap.S().Warnf("Skipping custom marker %s with unknown target type %s", marker.Name, marker.Target)
continue
}
if err := registry.Define(marker.Name, t, struct{}{}); err != nil {
return nil, fmt.Errorf("failed to define custom marker %s: %w", marker.Name, err)
}
}
return registry, nil
}
func parseMarkers(markers markers.MarkerValues) (string, []string) {
defaultValue := ""
validation := []string{}
markerNames := make([]string, 0, len(markers))
for name := range markers {
markerNames = append(markerNames, name)
}
sort.Strings(markerNames)
for _, name := range markerNames {
value := markers[name][len(markers[name])-1]
if strings.HasPrefix(name, "kubebuilder:validation:") {
name := strings.TrimPrefix(name, "kubebuilder:validation:")
switch name {
case "Pattern":
value = fmt.Sprintf("`%s`", value)
// FIXME: XValidation currently removed due to being long and difficult to read.
// E.g. "XValidation: {self.page < 200 Please start a new book.}"
case "XValidation":
continue
}
validation = append(validation, fmt.Sprintf("%s: %v", name, value))
}
if name == "kubebuilder:default" {
if value, ok := value.(crdmarkers.Default); ok {
defaultValue = fmt.Sprintf("%v", value.Value)
if strings.HasPrefix(defaultValue, "map[") {
defaultValue = strings.TrimPrefix(defaultValue, "map[")
defaultValue = strings.TrimSuffix(defaultValue, "]")
defaultValue = fmt.Sprintf("{ %s }", defaultValue)
}
}
}
}
return defaultValue, validation
}
func (p *processor) parseMarkers() {
for _, t := range p.types {
t.Default, t.Validation = parseMarkers(t.Markers)
for _, f := range t.Fields {
f.Default, f.Validation = parseMarkers(f.Markers)
}
}
}
func lookupConstantValuesForAliasedType(pkg *loader.Package, aliasTypeName string) []types.EnumValue {
values := []types.EnumValue{}
for _, file := range pkg.Syntax {
for _, decl := range file.Decls {
node, ok := decl.(*ast.GenDecl)
if !ok || node.Tok != token.CONST {
continue
}
for _, spec := range node.Specs {
// look for constant declaration
v, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
// value type must match the alias type name and have exactly one value
if id, ok := v.Type.(*ast.Ident); !ok || id.String() != aliasTypeName || len(v.Values) != 1 {
continue
}
// convert to a basic type to access to the value
b, ok := v.Values[0].(*ast.BasicLit)
if !ok {
continue
}
values = append(values, types.EnumValue{
// remove the '"' signs from the start and end of the value
Name: b.Value[1 : len(b.Value)-1],
Doc: v.Doc.Text(),
})
}
}
}
return values
}