syft/internal/capabilities/generate/metadata_check.go
Alex Goodman 1510db7c4e add info command from generated capabilities
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-10-13 17:14:40 -04:00

372 lines
11 KiB
Go

package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"sort"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/anchore/syft/internal/capabilities"
)
var (
warningStyleMeta = lipgloss.NewStyle().Foreground(lipgloss.Color("11")).Bold(true) // yellow
dimStyleMeta = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) // lighter grey (256-color)
// exceptions for metadata types that are intentionally not referenced in packages.yaml
metadataTypeExceptions = map[string]bool{
"pkg.MicrosoftKbPatch": true,
}
// exceptions for package types that are intentionally not referenced in packages.yaml
packageTypeExceptions = map[string]bool{
"jenkins-plugin": true,
"msrc-kb": true,
}
)
// parsePackageMetadataTypes parses packagemetadata/generated.go and extracts all metadata type names
// from the AllTypes() function (e.g., "pkg.AlpmDBEntry", "pkg.ApkDBEntry", etc.)
func parsePackageMetadataTypes(repoRoot string) ([]string, error) {
metadataFile := filepath.Join(repoRoot, "internal", "packagemetadata", "generated.go")
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, metadataFile, nil, 0)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", metadataFile, err)
}
var types []string
// find the AllTypes function
for _, decl := range f.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if !ok || funcDecl.Name.Name != "AllTypes" {
continue
}
// walk the function body to find return statement
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
returnStmt, ok := n.(*ast.ReturnStmt)
if !ok {
return true
}
// should have one return value: []any{...}
if len(returnStmt.Results) != 1 {
return true
}
// parse the composite literal (slice)
compositeLit, ok := returnStmt.Results[0].(*ast.CompositeLit)
if !ok {
return true
}
// extract each element (should be pkg.TypeName{})
for _, elt := range compositeLit.Elts {
if typeExpr, ok := elt.(*ast.CompositeLit); ok {
typeName := extractTypeName(typeExpr.Type)
if typeName != "" {
types = append(types, typeName)
}
}
}
return false
})
}
return types, nil
}
// extractTypeName extracts the full type name from an AST type expression
// e.g., pkg.AlpmDBEntry -> "pkg.AlpmDBEntry"
func extractTypeName(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.SelectorExpr:
// pkg.TypeName
if pkgIdent, ok := t.X.(*ast.Ident); ok {
return fmt.Sprintf("%s.%s", pkgIdent.Name, t.Sel.Name)
}
case *ast.Ident:
// just TypeName
return t.Name
}
return ""
}
// collectReferencedMetadataTypes walks through all catalogers and collects
// all metadata types referenced in parser and cataloger-level metadata_types fields
func collectReferencedMetadataTypes(doc *capabilities.Document) []string {
typeSet := make(map[string]bool)
for _, cataloger := range doc.Catalogers {
// collect from parsers (for generic catalogers)
for _, parser := range cataloger.Parsers {
for _, metadataType := range parser.MetadataTypes {
typeSet[metadataType] = true
}
}
// collect from cataloger-level metadata_types (for custom catalogers)
for _, metadataType := range cataloger.MetadataTypes {
typeSet[metadataType] = true
}
}
// convert set to sorted slice
var types []string
for typeName := range typeSet {
types = append(types, typeName)
}
sort.Strings(types)
return types
}
// checkMetadataTypeCoverage compares metadata types from packagemetadata/generated.go
// with types referenced in packages.yaml and returns unreferenced types
func checkMetadataTypeCoverage(yamlPath string, repoRoot string) ([]string, error) {
// parse packagemetadata/generated.go to get all types
allTypes, err := parsePackageMetadataTypes(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to parse package metadata types: %w", err)
}
// load packages.yaml to get referenced types
doc, _, err := loadCapabilities(yamlPath)
if err != nil {
return nil, fmt.Errorf("failed to load packages.yaml: %w", err)
}
referencedTypes := collectReferencedMetadataTypes(doc)
// create a set of referenced types for quick lookup
referencedSet := make(map[string]bool)
for _, typeName := range referencedTypes {
referencedSet[typeName] = true
}
// find unreferenced types (excluding exceptions)
var unreferenced []string
for _, typeName := range allTypes {
if !referencedSet[typeName] && !metadataTypeExceptions[typeName] {
unreferenced = append(unreferenced, typeName)
}
}
return unreferenced, nil
}
// printMetadataTypeCoverageWarning prints a warning if there are metadata types
// from packagemetadata/generated.go that aren't referenced in packages.yaml
func printMetadataTypeCoverageWarning(yamlPath string, repoRoot string) {
unreferenced, err := checkMetadataTypeCoverage(yamlPath, repoRoot)
if err != nil {
// don't fail generation, just skip the check
fmt.Printf("%s Could not check metadata type coverage: %v\n", warningStyleMeta.Render("⚠"), err)
return
}
if len(unreferenced) > 0 {
fmt.Println()
fmt.Printf("%s %s metadata types from packagemetadata are not referenced in packages.yaml:\n",
warningStyleMeta.Render("⚠ INFO:"),
warningStyleMeta.Render(fmt.Sprintf("%d", len(unreferenced))))
for _, typeName := range unreferenced {
// extract just the type name without "pkg." prefix for cleaner output
shortName := strings.TrimPrefix(typeName, "pkg.")
fmt.Printf(" - %s\n", dimStyleMeta.Render(shortName))
}
fmt.Println()
fmt.Println(dimStyleMeta.Render("These types may be:"))
fmt.Println(dimStyleMeta.Render(" • Used in custom catalogers (which don't have metadata_types)"))
fmt.Println(dimStyleMeta.Render(" • Deprecated or unused"))
fmt.Println(dimStyleMeta.Render(" • Missing from cataloger pattern documentation"))
}
}
// parseConstValues extracts constant names to their string values from an AST file
func parseConstValues(f *ast.File) map[string]string {
constValues := make(map[string]string)
for _, decl := range f.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.CONST {
continue
}
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for i, ident := range valueSpec.Names {
if i < len(valueSpec.Values) {
if lit, ok := valueSpec.Values[i].(*ast.BasicLit); ok && lit.Kind == token.STRING {
constValues[ident.Name] = strings.Trim(lit.Value, `"`)
}
}
}
}
}
return constValues
}
// extractTypesFromCompositeLit extracts package type names from a composite literal
func extractTypesFromCompositeLit(compositeLit *ast.CompositeLit, constValues map[string]string) []string {
var types []string
for _, elt := range compositeLit.Elts {
if ident, ok := elt.(*ast.Ident); ok {
// look up the string value for this constant
if typeName, ok := constValues[ident.Name]; ok && typeName != "UnknownPackage" {
types = append(types, typeName)
}
}
}
return types
}
// extractAllPkgsTypes finds the AllPkgs variable and extracts package type names
func extractAllPkgsTypes(f *ast.File, constValues map[string]string) []string {
for _, decl := range f.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.VAR {
continue
}
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for i, ident := range valueSpec.Names {
if ident.Name == "AllPkgs" && i < len(valueSpec.Values) {
// found AllPkgs, extract the slice elements
compositeLit, ok := valueSpec.Values[i].(*ast.CompositeLit)
if !ok {
continue
}
return extractTypesFromCompositeLit(compositeLit, constValues)
}
}
}
}
return []string{}
}
// parseAllPackageTypes parses syft/pkg/type.go and extracts all package type names
// from the AllPkgs variable by looking up their const string values
func parseAllPackageTypes(repoRoot string) ([]string, error) {
typeFile := filepath.Join(repoRoot, "syft", "pkg", "type.go")
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, typeFile, nil, 0)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", typeFile, err)
}
// first, build a map of constant names to their string values
constValues := parseConstValues(f)
// find the AllPkgs variable and extract types
types := extractAllPkgsTypes(f, constValues)
return types, nil
}
// collectReferencedPackageTypes walks through all catalogers and collects
// all package types referenced in parser and cataloger-level package_types fields
func collectReferencedPackageTypes(doc *capabilities.Document) []string {
typeSet := make(map[string]bool)
for _, cataloger := range doc.Catalogers {
// collect from parsers (for generic catalogers)
for _, parser := range cataloger.Parsers {
for _, pkgType := range parser.PackageTypes {
typeSet[pkgType] = true
}
}
// collect from cataloger-level package_types (for custom catalogers)
for _, pkgType := range cataloger.PackageTypes {
typeSet[pkgType] = true
}
}
// convert set to sorted slice
var types []string
for typeName := range typeSet {
types = append(types, typeName)
}
sort.Strings(types)
return types
}
// checkPackageTypeCoverage compares package types from pkg.AllPkgs
// with types referenced in packages.yaml and returns unreferenced types
func checkPackageTypeCoverage(yamlPath string, repoRoot string) ([]string, error) {
// parse pkg/type.go to get all package types
allTypes, err := parseAllPackageTypes(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to parse package types: %w", err)
}
// load packages.yaml to get referenced types
doc, _, err := loadCapabilities(yamlPath)
if err != nil {
return nil, fmt.Errorf("failed to load packages.yaml: %w", err)
}
referencedTypes := collectReferencedPackageTypes(doc)
// create a set of referenced types for quick lookup
referencedSet := make(map[string]bool)
for _, typeName := range referencedTypes {
referencedSet[typeName] = true
}
// find unreferenced types (excluding exceptions)
var unreferenced []string
for _, typeName := range allTypes {
if !referencedSet[typeName] && !packageTypeExceptions[typeName] {
unreferenced = append(unreferenced, typeName)
}
}
return unreferenced, nil
}
// printPackageTypeCoverageWarning prints a warning if there are package types
// from pkg.AllPkgs that aren't referenced in packages.yaml
func printPackageTypeCoverageWarning(yamlPath string, repoRoot string) {
unreferenced, err := checkPackageTypeCoverage(yamlPath, repoRoot)
if err != nil {
// don't fail generation, just skip the check
fmt.Printf("%s Could not check package type coverage: %v\n", warningStyleMeta.Render("⚠"), err)
return
}
if len(unreferenced) > 0 {
fmt.Println()
fmt.Printf("%s %s package types from pkg.AllPkgs are not referenced in packages.yaml:\n",
warningStyleMeta.Render("⚠ WARNING:"),
warningStyleMeta.Render(fmt.Sprintf("%d", len(unreferenced))))
for _, typeName := range unreferenced {
fmt.Printf(" - %s\n", dimStyleMeta.Render(typeName))
}
fmt.Println()
fmt.Println(dimStyleMeta.Render("These package types should be documented in packages.yaml."))
fmt.Println(dimStyleMeta.Render("If a package type is not emitted by any cataloger, it may be deprecated or unused."))
}
}