github.com/anchore/syft@v1.38.2/internal/jsonschema/comments.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/parser"
     7  	"go/token"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/invopop/jsonschema"
    13  )
    14  
    15  func copyAliasFieldComments(commentMap map[string]string, repoRoot string) {
    16  	// find all type aliases by parsing Go source files
    17  	aliases := findTypeAliases(repoRoot)
    18  
    19  	// for each alias, copy field comments from the source type
    20  	for aliasName, sourceName := range aliases {
    21  		// find all field comments for the source type
    22  		for key, comment := range commentMap {
    23  			// check if this is a field comment for the source type
    24  			// format: "github.com/anchore/syft/syft/pkg.SourceType.FieldName"
    25  			if strings.Contains(key, "."+sourceName+".") {
    26  				// create the corresponding key for the alias
    27  				aliasKey := strings.Replace(key, "."+sourceName+".", "."+aliasName+".", 1)
    28  				commentMap[aliasKey] = comment
    29  			}
    30  		}
    31  	}
    32  }
    33  
    34  func findTypeAliases(repoRoot string) map[string]string {
    35  	aliases := make(map[string]string)
    36  	fset := token.NewFileSet()
    37  
    38  	// walk through all Go files in the repo
    39  	err := filepath.Walk(repoRoot, func(path string, info os.FileInfo, err error) error {
    40  		if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
    41  			return nil
    42  		}
    43  
    44  		// parse the file
    45  		file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
    46  		if err != nil {
    47  			return nil
    48  		}
    49  
    50  		// look for type alias declarations
    51  		ast.Inspect(file, func(n ast.Node) bool {
    52  			typeSpec, ok := n.(*ast.TypeSpec)
    53  			if !ok {
    54  				return true
    55  			}
    56  
    57  			// check if this is a type alias (e.g., type A B where B is an identifier)
    58  			ident, ok := typeSpec.Type.(*ast.Ident)
    59  			if !ok {
    60  				return true
    61  			}
    62  
    63  			// store the alias mapping: aliasName -> sourceName
    64  			aliases[typeSpec.Name.Name] = ident.Name
    65  			return true
    66  		})
    67  
    68  		return nil
    69  	})
    70  
    71  	if err != nil {
    72  		fmt.Fprintf(os.Stderr, "error: failed to find type aliases: %v\n", err)
    73  		panic(err)
    74  	}
    75  
    76  	return aliases
    77  }
    78  
    79  func hasDescriptionInAlternatives(schema *jsonschema.Schema) bool {
    80  	// check oneOf alternatives
    81  	for _, alt := range schema.OneOf {
    82  		if alt.Description != "" {
    83  			return true
    84  		}
    85  	}
    86  	// check anyOf alternatives
    87  	for _, alt := range schema.AnyOf {
    88  		if alt.Description != "" {
    89  			return true
    90  		}
    91  	}
    92  	return false
    93  }
    94  
    95  func warnMissingDescriptions(schema *jsonschema.Schema, metadataNames []string) { //nolint:gocognit
    96  	var missingTypeDescriptions []string
    97  	var missingFieldDescriptions []string
    98  
    99  	// check metadata types for missing descriptions
   100  	for _, name := range metadataNames {
   101  		def, ok := schema.Definitions[name]
   102  		if !ok {
   103  			continue
   104  		}
   105  
   106  		// check if type has a description
   107  		if def.Description == "" {
   108  			missingTypeDescriptions = append(missingTypeDescriptions, name)
   109  		}
   110  
   111  		// check if fields have descriptions
   112  		if def.Properties != nil {
   113  			for _, fieldName := range def.Properties.Keys() {
   114  				fieldSchemaRaw, _ := def.Properties.Get(fieldName)
   115  				fieldSchema, ok := fieldSchemaRaw.(*jsonschema.Schema)
   116  				if !ok {
   117  					continue
   118  				}
   119  
   120  				// skip if field has a description
   121  				if fieldSchema.Description != "" {
   122  					continue
   123  				}
   124  
   125  				// skip if field is a reference (descriptions come from the referenced type)
   126  				if fieldSchema.Ref != "" {
   127  					continue
   128  				}
   129  
   130  				// skip if field is an array/object with items that are references
   131  				if fieldSchema.Items != nil && fieldSchema.Items.Ref != "" {
   132  					continue
   133  				}
   134  
   135  				// skip if field uses oneOf/anyOf with descriptions in the alternatives
   136  				if hasDescriptionInAlternatives(fieldSchema) {
   137  					continue
   138  				}
   139  
   140  				missingFieldDescriptions = append(missingFieldDescriptions, fmt.Sprintf("%s.%s", name, fieldName))
   141  			}
   142  		}
   143  	}
   144  
   145  	// report findings
   146  	if len(missingTypeDescriptions) > 0 {
   147  		fmt.Fprintf(os.Stderr, "\nwarning: %d metadata types are missing descriptions:\n", len(missingTypeDescriptions))
   148  		for _, name := range missingTypeDescriptions {
   149  			fmt.Fprintf(os.Stderr, "  - %s\n", name)
   150  		}
   151  	}
   152  
   153  	if len(missingFieldDescriptions) > 0 {
   154  		fmt.Fprintf(os.Stderr, "\nwarning: %d fields are missing descriptions:\n", len(missingFieldDescriptions))
   155  		for _, field := range missingFieldDescriptions {
   156  			fmt.Fprintf(os.Stderr, "  - %s\n", field)
   157  		}
   158  	}
   159  }