mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
* unexport as many types and functions from cataloger packages as possible Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * capture type and signature information in convention test Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * check that we return pkg.Cataloger from constructors Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
351 lines
9.0 KiB
Go
351 lines
9.0 KiB
Go
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/bmatcuk/doublestar/v4"
|
|
"github.com/scylladb/go-set/strset"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func Test_packageCatalogerExports(t *testing.T) {
|
|
// sanity check that we are actually finding exports
|
|
|
|
exports := packageCatalogerExports(t)
|
|
require.NotEmpty(t, exports)
|
|
|
|
expectAtLeast := map[string]*strset.Set{
|
|
"golang": strset.New("NewGoModuleFileCataloger", "NewGoModuleBinaryCataloger", "CatalogerConfig", "DefaultCatalogerConfig"),
|
|
}
|
|
|
|
for pkg, expected := range expectAtLeast {
|
|
actual, ok := exports[pkg]
|
|
require.True(t, ok, pkg)
|
|
require.True(t, expected.IsSubset(actual.Names()), pkg)
|
|
}
|
|
|
|
}
|
|
|
|
func Test_validatePackageCatalogerExport(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
export exportToken
|
|
wantErr assert.ErrorAssertionFunc
|
|
}{
|
|
// valid...
|
|
{
|
|
name: "valid constructor",
|
|
export: exportToken{
|
|
Name: "NewFooCataloger",
|
|
Type: "*ast.FuncType",
|
|
SignatureSize: 1,
|
|
ReturnTypeNames: []string{
|
|
"pkg.Cataloger",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "valid default config",
|
|
export: exportToken{
|
|
Name: "DefaultFooConfig",
|
|
Type: "*ast.FuncType",
|
|
SignatureSize: 0,
|
|
},
|
|
},
|
|
{
|
|
name: "valid config",
|
|
export: exportToken{
|
|
Name: "FooConfig",
|
|
Type: "*ast.StructType",
|
|
},
|
|
},
|
|
// invalid...
|
|
{
|
|
name: "constructor that returns a concrete type",
|
|
export: exportToken{
|
|
Name: "NewFooCataloger",
|
|
Type: "*ast.FuncType",
|
|
SignatureSize: 1,
|
|
ReturnTypeNames: []string{
|
|
"*generic.Cataloger",
|
|
},
|
|
},
|
|
wantErr: assert.Error,
|
|
},
|
|
{
|
|
name: "struct with constructor name",
|
|
export: exportToken{
|
|
Name: "NewFooCataloger",
|
|
Type: "*ast.StructType",
|
|
},
|
|
wantErr: assert.Error,
|
|
},
|
|
{
|
|
name: "struct with default config fn name",
|
|
export: exportToken{
|
|
Name: "DefaultFooConfig",
|
|
Type: "*ast.StructType",
|
|
},
|
|
wantErr: assert.Error,
|
|
},
|
|
{
|
|
name: "fn with struct name",
|
|
export: exportToken{
|
|
Name: "FooConfig",
|
|
Type: "*ast.FuncType",
|
|
},
|
|
wantErr: assert.Error,
|
|
},
|
|
{
|
|
name: "default config with parameters",
|
|
export: exportToken{
|
|
Name: "DefaultFooConfig",
|
|
Type: "*ast.FuncType",
|
|
SignatureSize: 1,
|
|
},
|
|
wantErr: assert.Error,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if c.wantErr == nil {
|
|
c.wantErr = assert.NoError
|
|
}
|
|
err := validatePackageCatalogerExport(t, "test", c.export)
|
|
c.wantErr(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_PackageCatalogerConventions(t *testing.T) {
|
|
// look at each package in syft/pkg/cataloger...
|
|
// we want to make certain that only the following things are exported from the package:
|
|
// - function matching New*Cataloger (e.g. NewAptCataloger)
|
|
// - function matching Default*Config
|
|
// - struct matching *Config
|
|
//
|
|
// anything else that is exported should result in the test failing.
|
|
// note: this is meant to apply to things in static space, not methods on structs or within interfaces.
|
|
//
|
|
// this additionally ensures that:
|
|
// - any config struct has a Default*Config function to pair with it.
|
|
// - all cataloger constructors return pkg.Cataloger interface instead of a concrete type
|
|
|
|
exportsPerPackage := packageCatalogerExports(t)
|
|
|
|
//for debugging purposes...
|
|
//for pkg, exports := range exportsPerPackage {
|
|
// t.Log(pkg)
|
|
// for _, export := range exports {
|
|
// t.Logf(" %#v", export)
|
|
// }
|
|
//}
|
|
|
|
for pkg, exports := range exportsPerPackage {
|
|
for _, export := range exports.List() {
|
|
// assert the export name is valid...
|
|
assert.NoError(t, validatePackageCatalogerExport(t, pkg, export))
|
|
|
|
// assert that config structs have a Default*Config functions to pair with them...
|
|
if strings.Contains(export.Name, "Config") && !strings.Contains(export.Name, "Default") {
|
|
// this is a config struct, make certain there is a pairing with a Default*Config function
|
|
assert.True(t, exports.Has("Default"+export.Name), "cataloger config struct %q in pkg %q must have a 'Default%s' function", export.Name, pkg, export.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func validatePackageCatalogerExport(t *testing.T, pkg string, export exportToken) error {
|
|
|
|
constructorMatches, err := doublestar.Match("New*Cataloger", export.Name)
|
|
require.NoError(t, err)
|
|
|
|
defaultConfigMatches, err := doublestar.Match("Default*Config", export.Name)
|
|
require.NoError(t, err)
|
|
|
|
configMatches, err := doublestar.Match("*Config", export.Name)
|
|
require.NoError(t, err)
|
|
|
|
switch {
|
|
case constructorMatches:
|
|
if !export.isFunction() {
|
|
return fmt.Errorf("constructor convention used for non-function in pkg=%q: %#v", pkg, export)
|
|
}
|
|
|
|
returnTypes := strset.New(export.ReturnTypeNames...)
|
|
if !returnTypes.Has("pkg.Cataloger") {
|
|
return fmt.Errorf("constructor convention is to return pkg.Cataloger and not concrete types. pkg=%q constructor=%q types=%+v", pkg, export.Name, strings.Join(export.ReturnTypeNames, ","))
|
|
}
|
|
|
|
case defaultConfigMatches:
|
|
if !export.isFunction() {
|
|
return fmt.Errorf("default config convention used for non-function in pkg=%q: %#v", pkg, export)
|
|
}
|
|
if export.SignatureSize != 0 {
|
|
return fmt.Errorf("default config convention used for non-zero signature size in pkg=%q: %#v", pkg, export)
|
|
}
|
|
case configMatches:
|
|
if !export.isStruct() {
|
|
return fmt.Errorf("config convention used for non-struct in pkg=%q: %#v", pkg, export)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unexpected export in pkg=%q: %#v", pkg, export)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type exportToken struct {
|
|
Name string
|
|
Type string
|
|
SignatureSize int
|
|
ReturnTypeNames []string
|
|
}
|
|
|
|
func (e exportToken) isFunction() bool {
|
|
return strings.Contains(e.Type, "ast.FuncType")
|
|
}
|
|
|
|
func (e exportToken) isStruct() bool {
|
|
return strings.Contains(e.Type, "ast.StructType")
|
|
}
|
|
|
|
type exportTokenSet map[string]exportToken
|
|
|
|
func (s exportTokenSet) Names() *strset.Set {
|
|
set := strset.New()
|
|
for k := range s {
|
|
set.Add(k)
|
|
}
|
|
return set
|
|
}
|
|
|
|
func (s exportTokenSet) Has(name string) bool {
|
|
_, ok := s[name]
|
|
return ok
|
|
}
|
|
|
|
func (s exportTokenSet) Add(tokens ...exportToken) {
|
|
for _, t := range tokens {
|
|
if _, ok := s[t.Name]; ok {
|
|
panic("duplicate token name: " + t.Name)
|
|
}
|
|
s[t.Name] = t
|
|
}
|
|
}
|
|
|
|
func (s exportTokenSet) Remove(names ...string) {
|
|
for _, name := range names {
|
|
delete(s, name)
|
|
}
|
|
}
|
|
|
|
func (s exportTokenSet) List() []exportToken {
|
|
var tokens []exportToken
|
|
for _, t := range s {
|
|
tokens = append(tokens, t)
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
func packageCatalogerExports(t *testing.T) map[string]exportTokenSet {
|
|
t.Helper()
|
|
|
|
catalogerPath := filepath.Join(repoRoot(t), "syft", "pkg", "cataloger")
|
|
|
|
ignorePaths := []string{
|
|
filepath.Join(catalogerPath, "common"),
|
|
filepath.Join(catalogerPath, "generic"),
|
|
}
|
|
|
|
exportsPerPackage := make(map[string]exportTokenSet)
|
|
|
|
err := filepath.Walk(catalogerPath, func(path string, info os.FileInfo, err error) error {
|
|
require.NoError(t, err)
|
|
|
|
if info.IsDir() ||
|
|
!strings.HasSuffix(info.Name(), ".go") ||
|
|
strings.HasSuffix(info.Name(), "_test.go") ||
|
|
strings.Contains(path, "test-fixtures") ||
|
|
strings.Contains(path, "internal") {
|
|
return nil
|
|
}
|
|
|
|
for _, ignorePath := range ignorePaths {
|
|
if strings.Contains(path, ignorePath) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
node, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
|
require.NoError(t, err)
|
|
|
|
pkg := node.Name.Name
|
|
for _, f := range node.Decls {
|
|
switch decl := f.(type) {
|
|
case *ast.GenDecl:
|
|
for _, spec := range decl.Specs {
|
|
switch spec := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
if spec.Name.IsExported() {
|
|
if _, ok := exportsPerPackage[pkg]; !ok {
|
|
exportsPerPackage[pkg] = make(exportTokenSet)
|
|
}
|
|
exportsPerPackage[pkg].Add(exportToken{
|
|
Name: spec.Name.Name,
|
|
Type: reflect.TypeOf(spec.Type).String(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
case *ast.FuncDecl:
|
|
if decl.Recv == nil && decl.Name.IsExported() {
|
|
var returnTypes []string
|
|
if decl.Type.Results != nil {
|
|
for _, field := range decl.Type.Results.List {
|
|
// TODO: there is probably a better way to extract the specific type name
|
|
//ty := strings.Join(strings.Split(fmt.Sprint(field.Type), " "), ".")
|
|
ty := types.ExprString(field.Type)
|
|
|
|
returnTypes = append(returnTypes, ty)
|
|
}
|
|
}
|
|
|
|
if _, ok := exportsPerPackage[pkg]; !ok {
|
|
exportsPerPackage[pkg] = make(exportTokenSet)
|
|
}
|
|
exportsPerPackage[pkg].Add(exportToken{
|
|
Name: decl.Name.Name,
|
|
Type: reflect.TypeOf(decl.Type).String(),
|
|
SignatureSize: len(decl.Type.Params.List),
|
|
ReturnTypeNames: returnTypes,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
// remove exceptions
|
|
// these are known violations to the common convention that are allowed.
|
|
if vs, ok := exportsPerPackage["binary"]; ok {
|
|
vs.Remove("Classifier", "EvidenceMatcher", "FileContentsVersionMatcher", "DefaultClassifiers")
|
|
}
|
|
|
|
return exportsPerPackage
|
|
}
|