| 1 | // repolint enforces the repo standards that gofmt/vet/golangci cannot express. |
| 2 | package main |
| 3 | |
| 4 | import ( |
| 5 | "flag" |
| 6 | "fmt" |
| 7 | "maps" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "slices" |
| 11 | "sort" |
| 12 | ) |
| 13 | |
| 14 | type Finding struct { |
| 15 | File string |
| 16 | Line int |
| 17 | Rule string |
| 18 | Msg string |
| 19 | // Excess over the rule's limit, so trading a short violation for a long |
| 20 | // one still trips the ratchet. One for rules that are pass/fail. |
| 21 | Weight int |
| 22 | } |
| 23 | |
| 24 | const ( |
| 25 | ruleEssay = "essay" |
| 26 | ruleBanner = "banner" |
| 27 | ruleMarker = "marker" |
| 28 | ruleDeadCode = "commented-code" |
| 29 | ruleNarrative = "narrative" |
| 30 | ruleFileSize = "file-size" |
| 31 | ruleTestSize = "test-file-size" |
| 32 | ruleLayering = "layering" |
| 33 | ruleFuncSize = "function-size" |
| 34 | ruleComplexity = "complexity" |
| 35 | ruleStructState = "struct-state" |
| 36 | ) |
| 37 | |
| 38 | var allRules = []string{ |
| 39 | ruleEssay, ruleBanner, ruleMarker, ruleDeadCode, |
| 40 | ruleNarrative, ruleFileSize, ruleTestSize, ruleLayering, |
| 41 | ruleFuncSize, ruleComplexity, ruleStructState, |
| 42 | } |
| 43 | |
| 44 | func main() { |
| 45 | root := flag.String("root", ".", "repository root to scan") |
| 46 | baselinePath := flag.String("baseline", "", "baseline file (default <root>/tools/repolint/baseline.json)") |
| 47 | update := flag.Bool("update", false, "rewrite the baseline from the current tree") |
| 48 | strict := flag.Bool("strict", false, "report every finding, ignoring the baseline") |
| 49 | flag.Parse() |
| 50 | |
| 51 | if *baselinePath == "" { |
| 52 | *baselinePath = filepath.Join(*root, "tools", "repolint", "baseline.json") |
| 53 | } |
| 54 | |
| 55 | findings, err := run(*root) |
| 56 | if err != nil { |
| 57 | fmt.Fprintln(os.Stderr, "repolint:", err) |
| 58 | os.Exit(2) |
| 59 | } |
| 60 | sort.Slice(findings, func(i, j int) bool { |
| 61 | if findings[i].File != findings[j].File { |
| 62 | return findings[i].File < findings[j].File |
| 63 | } |
| 64 | return findings[i].Line < findings[j].Line |
| 65 | }) |
| 66 | |
| 67 | if *update { |
| 68 | if err := baselineFrom(findings).write(*baselinePath); err != nil { |
| 69 | fmt.Fprintln(os.Stderr, "repolint:", err) |
| 70 | os.Exit(2) |
| 71 | } |
| 72 | fmt.Printf("wrote %s (%d findings across %d files)\n", *baselinePath, len(findings), countFiles(findings)) |
| 73 | return |
| 74 | } |
| 75 | |
| 76 | if *strict { |
| 77 | report(findings) |
| 78 | fmt.Printf("\n%d findings across %d files\n", len(findings), countFiles(findings)) |
| 79 | if len(findings) > 0 { |
| 80 | os.Exit(1) |
| 81 | } |
| 82 | return |
| 83 | } |
| 84 | |
| 85 | baseline, err := loadBaseline(*baselinePath) |
| 86 | if err != nil { |
| 87 | fmt.Fprintln(os.Stderr, "repolint:", err) |
| 88 | os.Exit(2) |
| 89 | } |
| 90 | over, msgs := baseline.exceeded(findings) |
| 91 | if len(msgs) == 0 { |
| 92 | fmt.Printf("repolint: clean (%d baselined findings)\n", len(findings)) |
| 93 | return |
| 94 | } |
| 95 | report(over) |
| 96 | fmt.Fprintln(os.Stderr) |
| 97 | for _, m := range msgs { |
| 98 | fmt.Fprintln(os.Stderr, "repolint:", m) |
| 99 | } |
| 100 | fmt.Fprintf(os.Stderr, "\nNew standards violations. Fix them, or if this is a deliberate\n"+ |
| 101 | "carry-forward (file rename, extraction), run:\n\n go run ./tools/repolint -update\n\n"+ |
| 102 | "and justify the baseline diff in the pull request.\n") |
| 103 | os.Exit(1) |
| 104 | } |
| 105 | |
| 106 | func run(root string) ([]Finding, error) { |
| 107 | paths, err := collect(root) |
| 108 | if err != nil { |
| 109 | return nil, err |
| 110 | } |
| 111 | var findings []Finding |
| 112 | imports := map[string][]importRef{} |
| 113 | for _, rel := range paths { |
| 114 | src, err := parseSource(root, rel) |
| 115 | if err != nil { |
| 116 | return nil, err |
| 117 | } |
| 118 | if src == nil { |
| 119 | continue |
| 120 | } |
| 121 | findings = append(findings, checkSize(src)...) |
| 122 | if src.file == nil { |
| 123 | continue |
| 124 | } |
| 125 | findings = append(findings, checkComments(src)...) |
| 126 | findings = append(findings, checkComplexity(src)...) |
| 127 | findings = append(findings, checkStructState(src)...) |
| 128 | imports[rel] = src.importRefs() |
| 129 | } |
| 130 | return append(findings, checkLayering(imports)...), nil |
| 131 | } |
| 132 | |
| 133 | func report(findings []Finding) { |
| 134 | for _, f := range findings { |
| 135 | fmt.Fprintf(os.Stderr, "%s:%d: [%s] %s\n", f.File, f.Line, f.Rule, f.Msg) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | func countFiles(findings []Finding) int { |
| 140 | seen := map[string]bool{} |
| 141 | for _, f := range findings { |
| 142 | seen[f.File] = true |
| 143 | } |
| 144 | return len(seen) |
| 145 | } |
| 146 | |
| 147 | func sortedKeys[V any](m map[string]V) []string { |
| 148 | return slices.Sorted(maps.Keys(m)) |
| 149 | } |
| 150 |