package depguard import ( "go/build" "go/token" "io/ioutil" "path" "sort" "strings" "github.com/gobwas/glob" "golang.org/x/tools/go/loader" ) // ListType states what kind of list is passed in. type ListType int const ( // LTBlacklist states the list given is a blacklist. (default) LTBlacklist ListType = iota // LTWhitelist states the list given is a whitelist. LTWhitelist ) // StringToListType makes it easier to turn a string into a ListType. // It assumes that the string representation is lower case. var StringToListType = map[string]ListType{ "whitelist": LTWhitelist, "blacklist": LTBlacklist, } // Issue with the package with PackageName at the Position. type Issue struct { PackageName string Position token.Position } // Depguard checks imports to make sure they follow the given list and constraints. type Depguard struct { ListType ListType IncludeGoRoot bool Packages []string prefixPackages []string globPackages []glob.Glob TestPackages []string prefixTestPackages []string globTestPackages []glob.Glob prefixRoot []string } // Run checks for dependencies given the program and validates them against // Packages. func (dg *Depguard) Run(config *loader.Config, prog *loader.Program) ([]*Issue, error) { // Shortcut execution on an empty blacklist as that means every package is allowed if dg.ListType == LTBlacklist && len(dg.Packages) == 0 { return nil, nil } if err := dg.initialize(config, prog); err != nil { return nil, err } directImports, err := dg.createImportMap(prog) if err != nil { return nil, err } var issues []*Issue for pkg, positions := range directImports { for _, pos := range positions { prefixList, globList := dg.prefixPackages, dg.globPackages if len(dg.TestPackages) > 0 && strings.Index(pos.Filename, "_test.go") != -1 { prefixList, globList = dg.prefixTestPackages, dg.globTestPackages } if dg.flagIt(pkg, prefixList, globList) { issues = append(issues, &Issue{ PackageName: pkg, Position: pos, }) } } } return issues, nil } func (dg *Depguard) initialize(config *loader.Config, prog *loader.Program) error { // parse ordinary guarded packages for _, pkg := range dg.Packages { if strings.ContainsAny(pkg, "!?*[]{}") { g, err := glob.Compile(pkg, '/') if err != nil { return err } dg.globPackages = append(dg.globPackages, g) } else { dg.prefixPackages = append(dg.prefixPackages, pkg) } } // Sort the packages so we can have a faster search in the array sort.Strings(dg.prefixPackages) // parse guarded tests packages for _, pkg := range dg.TestPackages { if strings.ContainsAny(pkg, "!?*[]{}") { g, err := glob.Compile(pkg, '/') if err != nil { return err } dg.globTestPackages = append(dg.globTestPackages, g) } else { dg.prefixTestPackages = append(dg.prefixTestPackages, pkg) } } // Sort the test packages so we can have a faster search in the array sort.Strings(dg.prefixTestPackages) if !dg.IncludeGoRoot { var err error dg.prefixRoot, err = listRootPrefixs(config.Build) if err != nil { return err } } return nil } func (dg *Depguard) createImportMap(prog *loader.Program) (map[string][]token.Position, error) { importMap := make(map[string][]token.Position) // For the directly imported packages for _, imported := range prog.InitialPackages() { // Go through their files for _, file := range imported.Files { // And populate a map of all direct imports and their positions // This will filter out GoRoot depending on the Depguard.IncludeGoRoot for _, fileImport := range file.Imports { fileImportPath := cleanBasicLitString(fileImport.Path.Value) if !dg.IncludeGoRoot && dg.isRoot(fileImportPath) { continue } position := prog.Fset.Position(fileImport.Pos()) positions, found := importMap[fileImportPath] if !found { importMap[fileImportPath] = []token.Position{ position, } continue } importMap[fileImportPath] = append(positions, position) } } } return importMap, nil } func pkgInList(pkg string, prefixList []string, globList []glob.Glob) bool { if pkgInPrefixList(pkg, prefixList) { return true } return pkgInGlobList(pkg, globList) } func pkgInPrefixList(pkg string, prefixList []string) bool { // Idx represents where in the package slice the passed in package would go // when sorted. -1 Just means that it would be at the very front of the slice. idx := sort.Search(len(prefixList), func(i int) bool { return prefixList[i] > pkg }) - 1 // This means that the package passed in has no way to be prefixed by anything // in the package list as it is already smaller then everything if idx == -1 { return false } return strings.HasPrefix(pkg, prefixList[idx]) } func pkgInGlobList(pkg string, globList []glob.Glob) bool { for _, g := range globList { if g.Match(pkg) { return true } } return false } // InList | WhiteList | BlackList // y | | x // n | x | func (dg *Depguard) flagIt(pkg string, prefixList []string, globList []glob.Glob) bool { return pkgInList(pkg, prefixList, globList) == (dg.ListType == LTBlacklist) } func cleanBasicLitString(value string) string { return strings.Trim(value, "\"\\") } // We can do this as all imports that are not root are either prefixed with a domain // or prefixed with `./` or `/` to dictate it is a local file reference func listRootPrefixs(buildCtx *build.Context) ([]string, error) { if buildCtx == nil { buildCtx = &build.Default } root := path.Join(buildCtx.GOROOT, "src") fs, err := ioutil.ReadDir(root) if err != nil { return nil, err } var pkgPrefix []string for _, f := range fs { if !f.IsDir() { continue } pkgPrefix = append(pkgPrefix, f.Name()) } return pkgPrefix, nil } func (dg *Depguard) isRoot(importPath string) bool { // Idx represents where in the package slice the passed in package would go // when sorted. -1 Just means that it would be at the very front of the slice. idx := sort.Search(len(dg.prefixRoot), func(i int) bool { return dg.prefixRoot[i] > importPath }) - 1 // This means that the package passed in has no way to be prefixed by anything // in the package list as it is already smaller then everything if idx == -1 { return false } // if it is prefixed by a root prefix we need to check if it is an exact match // or prefix with `/` as this could return false posative if the domain was // `archive.com` for example as `archive` is a go root package. if strings.HasPrefix(importPath, dg.prefixRoot[idx]) { return strings.HasPrefix(importPath, dg.prefixRoot[idx]+"/") || importPath == dg.prefixRoot[idx] } return false }