package gomodguard import ( "flag" "fmt" "io/ioutil" "log" "os" "path/filepath" "strings" "github.com/go-xmlfmt/xmlfmt" "github.com/mitchellh/go-homedir" "github.com/phayes/checkstyle" "gopkg.in/yaml.v2" ) const ( errFindingHomedir = "unable to find home directory, %w" errReadingConfigFile = "could not read config file: %w" errParsingConfigFile = "could not parse config file: %w" ) var ( configFile = ".gomodguard.yaml" logger = log.New(os.Stderr, "", 0) errFindingConfigFile = fmt.Errorf("could not find config file") ) // Run the gomodguard linter. Returns the exit code to use. func Run() int { var ( args []string help bool noTest bool report string reportFile string issuesExitCode int cwd, _ = os.Getwd() ) flag.BoolVar(&help, "h", false, "Show this help text") flag.BoolVar(&help, "help", false, "") flag.BoolVar(&noTest, "n", false, "Don't lint test files") flag.BoolVar(&noTest, "no-test", false, "") flag.StringVar(&report, "r", "", "Report results to one of the following formats: checkstyle. A report file destination must also be specified") flag.StringVar(&report, "report", "", "") flag.StringVar(&reportFile, "f", "", "Report results to the specified file. A report type must also be specified") flag.StringVar(&reportFile, "file", "", "") flag.IntVar(&issuesExitCode, "i", 2, "Exit code when issues were found") flag.IntVar(&issuesExitCode, "issues-exit-code", 2, "") flag.Parse() report = strings.TrimSpace(strings.ToLower(report)) if help { showHelp() return 0 } if report != "" && report != "checkstyle" { logger.Fatalf("error: invalid report type '%s'", report) } if report != "" && reportFile == "" { logger.Fatalf("error: a report file must be specified when a report is enabled") } if report == "" && reportFile != "" { logger.Fatalf("error: a report type must be specified when a report file is enabled") } args = flag.Args() if len(args) == 0 { args = []string{"./..."} } config, err := GetConfig(configFile) if err != nil { logger.Fatalf("error: %s", err) } filteredFiles := GetFilteredFiles(cwd, noTest, args) processor, err := NewProcessor(config) if err != nil { logger.Fatalf("error: %s", err) } logger.Printf("info: allowed modules, %+v", config.Allowed.Modules) logger.Printf("info: allowed module domains, %+v", config.Allowed.Domains) logger.Printf("info: blocked modules, %+v", config.Blocked.Modules.Get()) logger.Printf("info: blocked modules with version constraints, %+v", config.Blocked.Versions.Get()) results := processor.ProcessFiles(filteredFiles) if report == "checkstyle" { err := WriteCheckstyle(reportFile, results) if err != nil { logger.Fatalf("error: %s", err) } } for _, r := range results { fmt.Println(r.String()) } if len(results) > 0 { return issuesExitCode } return 0 } // GetConfig from YAML file. func GetConfig(configFile string) (*Configuration, error) { config := Configuration{} home, err := homedir.Dir() if err != nil { return nil, fmt.Errorf(errFindingHomedir, err) } cfgFile := "" homeDirCfgFile := filepath.Join(home, configFile) switch { case fileExists(configFile): cfgFile = configFile case fileExists(homeDirCfgFile): cfgFile = homeDirCfgFile default: return nil, fmt.Errorf("%w: %s %s", errFindingConfigFile, configFile, homeDirCfgFile) } data, err := ioutil.ReadFile(cfgFile) if err != nil { return nil, fmt.Errorf(errReadingConfigFile, err) } err = yaml.Unmarshal(data, &config) if err != nil { return nil, fmt.Errorf(errParsingConfigFile, err) } return &config, nil } // GetFilteredFiles returns files based on search string arguments and filters. func GetFilteredFiles(cwd string, skipTests bool, args []string) []string { var ( foundFiles = []string{} filteredFiles = []string{} ) for _, f := range args { if strings.HasSuffix(f, "/...") { dir, _ := filepath.Split(f) foundFiles = append(foundFiles, expandGoWildcard(dir)...) continue } if _, err := os.Stat(f); err == nil { foundFiles = append(foundFiles, f) } } // Use relative path to print shorter names, sort out test foundFiles if chosen. for _, f := range foundFiles { if skipTests { if strings.HasSuffix(f, "_test.go") { continue } } if relativePath, err := filepath.Rel(cwd, f); err == nil { filteredFiles = append(filteredFiles, relativePath) continue } filteredFiles = append(filteredFiles, f) } return filteredFiles } // showHelp text for command line. func showHelp() { helpText := `Usage: gomodguard [files...] Also supports package syntax but will use it in relative path, i.e. ./pkg/... Flags:` fmt.Println(helpText) flag.PrintDefaults() } // WriteCheckstyle takes the results and writes them to a checkstyle formated file. func WriteCheckstyle(checkstyleFilePath string, results []Issue) error { check := checkstyle.New() for i := range results { file := check.EnsureFile(results[i].FileName) file.AddError(checkstyle.NewError(results[i].LineNumber, 1, checkstyle.SeverityError, results[i].Reason, "gomodguard")) } checkstyleXML := fmt.Sprintf("\n%s", check.String()) err := ioutil.WriteFile(checkstyleFilePath, []byte(xmlfmt.FormatXML(checkstyleXML, "", " ")), 0644) // nolint:gosec if err != nil { return err } return nil } // fileExists returns true if the file path provided exists. func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() } // expandGoWildcard path provided. func expandGoWildcard(root string) []string { foundFiles := []string{} _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { // Only append go foundFiles. if !strings.HasSuffix(info.Name(), ".go") { return nil } foundFiles = append(foundFiles, path) return nil }) return foundFiles }