github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/schema/json/internal/metadata_types.go (about)

     1  package internal
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/parser"
     7  	"go/token"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"sort"
    11  	"strings"
    12  	"unicode"
    13  
    14  	"github.com/scylladb/go-set/strset"
    15  )
    16  
    17  var metadataExceptions = strset.New(
    18  	"FileMetadata",
    19  )
    20  
    21  func AllGosbomMetadataTypeNames() ([]string, error) {
    22  	root, err := repoRoot()
    23  	if err != nil {
    24  		return nil, err
    25  	}
    26  	files, err := filepath.Glob(filepath.Join(root, "gosbom/pkg/*.go"))
    27  	if err != nil {
    28  		return nil, err
    29  	}
    30  	return findMetadataDefinitionNames(files...)
    31  }
    32  
    33  func repoRoot() (string, error) {
    34  	root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
    35  	if err != nil {
    36  		return "", fmt.Errorf("unable to find repo root dir: %+v", err)
    37  	}
    38  	absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
    39  	if err != nil {
    40  		return "", fmt.Errorf("unable to get abs path to repo root: %w", err)
    41  	}
    42  	return absRepoRoot, nil
    43  }
    44  
    45  func findMetadataDefinitionNames(paths ...string) ([]string, error) {
    46  	names := strset.New()
    47  	usedNames := strset.New()
    48  	for _, path := range paths {
    49  		metadataDefinitions, usedTypeNames, err := findMetadataDefinitionNamesInFile(path)
    50  		if err != nil {
    51  			return nil, err
    52  		}
    53  
    54  		// useful for debugging...
    55  		// fmt.Println(path)
    56  		// fmt.Println("Defs:", metadataDefinitions)
    57  		// fmt.Println("Used Types:", usedTypeNames)
    58  		// fmt.Println()
    59  
    60  		names.Add(metadataDefinitions...)
    61  		usedNames.Add(usedTypeNames...)
    62  	}
    63  
    64  	// any definition that is used within another struct should not be considered a top-level metadata definition
    65  	names.Remove(usedNames.List()...)
    66  
    67  	strNames := names.List()
    68  	sort.Strings(strNames)
    69  
    70  	// note: 30 is a point-in-time gut check. This number could be updated if new metadata definitions are added, but is not required.
    71  	// it is really intended to catch any major issues with the generation process that would generate, say, 0 definitions.
    72  	if len(strNames) < 30 {
    73  		return nil, fmt.Errorf("not enough metadata definitions found (discovered: " + fmt.Sprintf("%d", len(strNames)) + ")")
    74  	}
    75  
    76  	return strNames, nil
    77  }
    78  
    79  func findMetadataDefinitionNamesInFile(path string) ([]string, []string, error) {
    80  	// set up the parser
    81  	fs := token.NewFileSet()
    82  	f, err := parser.ParseFile(fs, path, nil, parser.ParseComments)
    83  	if err != nil {
    84  		return nil, nil, err
    85  	}
    86  
    87  	var metadataDefinitions []string
    88  	var usedTypeNames []string
    89  	for _, decl := range f.Decls {
    90  		// check if the declaration is a type declaration
    91  		spec, ok := decl.(*ast.GenDecl)
    92  		if !ok || spec.Tok != token.TYPE {
    93  			continue
    94  		}
    95  
    96  		// loop over all types declared in the type declaration
    97  		for _, typ := range spec.Specs {
    98  			// check if the type is a struct type
    99  			spec, ok := typ.(*ast.TypeSpec)
   100  			if !ok || spec.Type == nil {
   101  				continue
   102  			}
   103  
   104  			structType, ok := spec.Type.(*ast.StructType)
   105  			if !ok {
   106  				continue
   107  			}
   108  
   109  			// check if the struct type ends with "Metadata"
   110  			name := spec.Name.String()
   111  
   112  			// only look for exported types that end with "Metadata"
   113  			if isMetadataTypeCandidate(name) {
   114  				// print the full declaration of the struct type
   115  				metadataDefinitions = append(metadataDefinitions, name)
   116  				usedTypeNames = append(usedTypeNames, typeNamesUsedInStruct(structType)...)
   117  			}
   118  		}
   119  	}
   120  	return metadataDefinitions, usedTypeNames, nil
   121  }
   122  
   123  func typeNamesUsedInStruct(structType *ast.StructType) []string {
   124  	// recursively find all type names used in the struct type
   125  	var names []string
   126  	for i := range structType.Fields.List {
   127  		// capture names of all of the types (not field names)
   128  		ast.Inspect(structType.Fields.List[i].Type, func(n ast.Node) bool {
   129  			ident, ok := n.(*ast.Ident)
   130  			if !ok {
   131  				return true
   132  			}
   133  
   134  			// add the type name to the list
   135  			names = append(names, ident.Name)
   136  
   137  			// continue inspecting
   138  			return true
   139  		})
   140  	}
   141  
   142  	return names
   143  }
   144  
   145  func isMetadataTypeCandidate(name string) bool {
   146  	return len(name) > 0 &&
   147  		strings.HasSuffix(name, "Metadata") &&
   148  		unicode.IsUpper(rune(name[0])) && // must be exported
   149  		!metadataExceptions.Has(name)
   150  }