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 }