package cli import ( "errors" "flag" "fmt" "io" "os" "path/filepath" "git.vakhrushev.me/av/convy/internal/check" "git.vakhrushev.me/av/convy/internal/manifest" "git.vakhrushev.me/av/convy/internal/suite" ) func runSuiteCheck(env Env, args []string) ExitCode { fs := flag.NewFlagSet("convy suite check", flag.ContinueOnError) fs.SetOutput(env.Err) root := fs.String("root", "", "root of the suite; by default it is looked up upwards from the current directory") quiet := fs.Bool("quiet", false, "print findings only") asJSON := fs.Bool("json", false, "write the findings as JSON, for a caller that is not a person") if err := fs.Parse(args); err != nil { return Usage } dir := *root if dir == "" { found, err := manifest.Find(env.Dir) if err != nil { if errors.Is(err, manifest.ErrNotFound) { fmt.Fprintf(env.Err, "not a conventions suite: no %s here or above\n", manifest.Name) if _, err := os.Stat(filepath.Join(env.Dir, ".conventions.toml")); err == nil { fmt.Fprintln(env.Err, "this is a project — checking what is here is called \"convy check\"") } return Usage } fmt.Fprintln(env.Err, err) return Usage } dir = found } s, err := suite.Load(dir) if err != nil { fmt.Fprintln(env.Err, err) return Usage } rep := check.Suite(s) if *asJSON { if code := printJSON(env, rep); code != OK { return code } } else { printReport(env.Out, rep, s, *quiet) } if rep.Errors() > 0 { return Failed } return OK } // printJSON writes the findings for a machine. It is the same report the person // gets, in the same order — a second answer that disagreed with the first would // be worse than no second answer. func printJSON(env Env, rep *check.Report) ExitCode { body, err := rep.JSON() if err != nil { fmt.Fprintln(env.Err, err) return Failed } env.Out.Write(body) return OK } func printReport(w io.Writer, rep *check.Report, s *suite.Suite, quiet bool) { findings := rep.Findings() printFindings(w, findings) if quiet { return } if len(findings) > 0 { fmt.Fprintln(w) } fmt.Fprintf(w, "suite: %s, %s, language version %d (%s)\n", plural(len(s.Docs), "file"), plural(len(s.Manifest.LiveTopics()), "topic"), s.Manifest.Language.Version, s.Manifest.Language.Lang) // A check that was skipped says so. Reaching the language costs a fetch and // this check runs on every edit, so the documents about the language go // unread — and a check silently not run reads exactly like a check passed. if spec := s.Manifest.Language.Source; spec != "" { fmt.Fprintf(w, "the language lies at %s and was not fetched: its documents went unchecked\n", spec) } switch { case rep.Errors() > 0: fmt.Fprintf(w, "errors: %d, warnings: %d\n", rep.Errors(), rep.Warnings()) case rep.Warnings() > 0: fmt.Fprintf(w, "no errors, warnings: %d\n", rep.Warnings()) default: fmt.Fprintln(w, "suite integrity holds") } } // plural agrees a noun with a count: 1 file, 4 files. func plural(n int, noun string) string { if n == 1 { return fmt.Sprintf("%d %s", n, noun) } return fmt.Sprintf("%d %ss", n, noun) } // printFindings lays the findings out grouped by file. Both checks print them // the same way: a finding of the suite and a finding of a copy are read by the // same person, and two layouts would be two things to learn. func printFindings(w io.Writer, findings []check.Finding) { current := "" for _, f := range findings { if f.Path != current { if current != "" { fmt.Fprintln(w) } fmt.Fprintf(w, "%s\n", f.Path) current = f.Path } where := "" if f.Line > 0 { where = fmt.Sprintf(":%d", f.Line) } fmt.Fprintf(w, " %s%s %s [%s]\n", f.Severity, where, f.Msg, f.Family) } }