diff --git a/v2/cmd/zlint/main.go b/v2/cmd/zlint/main.go index 679dbed81..7fe8f8f5d 100644 --- a/v2/cmd/zlint/main.go +++ b/v2/cmd/zlint/main.go @@ -31,11 +31,14 @@ import ( "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v2" "github.com/zmap/zlint/v2/lint" + "github.com/zmap/zlint/v2/formattedoutput" ) var ( // flags listLintsJSON bool listLintSources bool + summary bool + longSummary bool prettyprint bool format string nameFilter string @@ -51,6 +54,8 @@ var ( // flags func init() { flag.BoolVar(&listLintsJSON, "list-lints-json", false, "Print lints in JSON format, one per line") flag.BoolVar(&listLintSources, "list-lints-source", false, "Print list of lint sources, one per line") + flag.BoolVar(&summary, "summary", false, "Prints a short human-readable summary report") + flag.BoolVar(&longSummary, "longSummary", false, "Prints a human-readable summary report with details") flag.StringVar(&format, "format", "pem", "One of {pem, der, base64}") flag.StringVar(&nameFilter, "nameFilter", "", "Only run lints with a name matching the provided regex. (Can not be used with -includeNames/-excludeNames)") flag.StringVar(&includeNames, "includeNames", "", "Comma-separated list of lints to include by name") @@ -58,7 +63,7 @@ func init() { flag.StringVar(&includeSources, "includeSources", "", "Comma-separated list of lint sources to include") flag.StringVar(&excludeSources, "excludeSources", "", "Comma-separated list of lint sources to exclude") - flag.BoolVar(&prettyprint, "pretty", false, "Pretty-print output") + flag.BoolVar(&prettyprint, "pretty", false, "Pretty-print JSON output") flag.Usage = func() { fmt.Fprintf(os.Stderr, "ZLint version %s\n\n", version) fmt.Fprintf(os.Stderr, "Usage: %s [flags] file...\n", os.Args[0]) @@ -156,7 +161,15 @@ func doLint(inputFile *os.File, inform string, registry lint.Registry) { log.Fatalf("can't format output: %s", err) } os.Stdout.Write(out.Bytes()) - } else { + fmt.Printf("\n\n") + } + if summary { + formattedoutput.OutputSummary(zlintResult, false) + } + if longSummary { + formattedoutput.OutputSummary(zlintResult, true) + } + if !prettyprint && !summary && !longSummary { os.Stdout.Write(jsonBytes) } os.Stdout.Write([]byte{'\n'}) diff --git a/v2/formattedoutput/formattedOutput.go b/v2/formattedoutput/formattedOutput.go new file mode 100644 index 000000000..ea3f31fae --- /dev/null +++ b/v2/formattedoutput/formattedOutput.go @@ -0,0 +1,160 @@ +package formattedoutput + +import ( + "fmt" + "strconv" + "strings" + "unicode/utf8" + "sort" + + "github.com/zmap/zlint/v2" + "github.com/zmap/zlint/v2/lint" + +) + +type resultsTable struct { + resultCount map[lint.LintStatus]int + resultDetails map[lint.LintStatus][]string + lintLevelsAboveThreshold map[int]lint.LintStatus + sortedLevels []int +} + +func (r *resultsTable) newRT(threshold lint.LintStatus, results *zlint.ResultSet, longSummary bool) resultsTable { + + r.resultCount = make(map[lint.LintStatus]int) + r.resultDetails = make(map[lint.LintStatus][]string) + r.lintLevelsAboveThreshold = make(map[int]lint.LintStatus) + + // Make the list of lint levels that matter + for _, i := range lint.StatusLabelToLintStatus { + if i <= threshold { + continue + } + r.lintLevelsAboveThreshold[int(i)] = i + } + // Set all of the levels to 0 events so they are all displayed + // in the -summary table + for _, level := range r.lintLevelsAboveThreshold { + r.resultCount[level] = 0 + } + // Count up the number of each event + for lintName, lintResult := range results.Results { + if lintResult.Status > threshold { + r.resultCount[lintResult.Status]++ + if longSummary { + r.resultDetails[lintResult.Status] = append( + r.resultDetails[lintResult.Status], + string(lintName), + ) + } + } + } + // Sort the levels we have so we can get a nice output + for key := range r.resultCount { + r.sortedLevels = append(r.sortedLevels, int(key)) + } + sort.Ints(r.sortedLevels) + + return *r +} + + +func OutputSummary(zlintResult *zlint.ResultSet, longSummary bool) { + // Set the threashold under which (inclusive) events are not + // counted + threshold := lint.Pass + + rt := (&resultsTable{}).newRT(threshold, zlintResult, longSummary) + + // make and print the requested table type + if longSummary { + // make a table with the internal lint names grouped + // by type + var olsl string + headings := []string{ + "Level", + "# occurrences", + " Details ", + } + lines := [][]string{} + lsl := "" + rescount := "" + + hlengths := printTableHeadings(headings) + // Construct the table lines, but don't repeat + // LintStatus(level) or the results count. Also, just + // because a level wasn't seen doesn't mean it isn't + // important; display "empty" levels, too + for _, level := range rt.sortedLevels { + foundDetail := false + for _, detail := range rt.resultDetails[lint.LintStatus(level)] { + if fmt.Sprintf("%s", lint.LintStatus(level)) != olsl { + olsl = fmt.Sprintf("%s", lint.LintStatus(level)) + lsl = olsl + rescount = strconv.Itoa(rt.resultCount[lint.LintStatus(level)]) + } else { + lsl = "" + rescount = "" + } + lines = append(lines, ([]string{lsl, rescount, detail})) + foundDetail = true + } + if !foundDetail { + lines = append(lines, []string{ + fmt.Sprintf("%s", lint.LintStatus(level)), + strconv.Itoa(rt.resultCount[lint.LintStatus(level)]), + " - ", + }) + } + } + printTableBody(hlengths, lines) + } else { + headings := []string{"Level", "# occurrences"} + hlengths := printTableHeadings(headings) + lines := [][]string{} + for _, level := range rt.sortedLevels { + lines = append(lines, []string{ + fmt.Sprintf("%s", lint.LintStatus(level)), + strconv.Itoa(rt.resultCount[lint.LintStatus(level)])}) + } + printTableBody(hlengths, lines) + fmt.Printf("\n") + } +} + +func printTableHeadings(headings []string) []int { + hlengths := []int{} + for i, h := range headings { + hlengths = append( + hlengths, + utf8.RuneCountInString(h)+1) + fmt.Printf("| %s ", strings.ToUpper(h)) + if i == len(headings)-1 { + fmt.Printf("|\n") + for ii, j := range hlengths { + fmt.Printf("+%s", strings.Repeat("-", j+1)) + if ii == len(headings)-1 { + fmt.Printf("+\n") + } + } + } + } + return hlengths +} + +func printTableBody(hlengths []int, lines [][]string) { + for _, line := range lines { + for i, hlen := range hlengths { + // This makes a format string with the + // right widths, e.g. "%7.7s" + fmtstring := fmt.Sprintf("|%%%[1]d.%[1]ds", hlen) + fmt.Printf(fmtstring, line[i]) + if i == len(hlengths)-1 { + fmt.Printf(" |\n") + } else { + fmt.Printf(" ") + } + } + } + +} diff --git a/v2/lint/result.go b/v2/lint/result.go index fd9a1680d..96c43ab00 100644 --- a/v2/lint/result.go +++ b/v2/lint/result.go @@ -42,10 +42,10 @@ const ( ) var ( - // statusLabelToLintStatus is used to work backwards from + // StatusLabelToLintStatus is used to work backwards from // a LintStatus.String() to the LintStatus. This is used by // LintStatus.Unmarshal. - statusLabelToLintStatus = map[string]LintStatus{ + StatusLabelToLintStatus = map[string]LintStatus{ Reserved.String(): Reserved, NA.String(): NA, NE.String(): NE, @@ -73,7 +73,7 @@ func (e LintStatus) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the json.Unmarshaler interface. func (e *LintStatus) UnmarshalJSON(data []byte) error { key := strings.ReplaceAll(string(data), `"`, "") - if status, ok := statusLabelToLintStatus[key]; ok { + if status, ok := StatusLabelToLintStatus[key]; ok { *e = status } else { return fmt.Errorf("bad LintStatus JSON value: %s", string(data))