mirror of
https://github.com/gojp/goreportcard.git
synced 2026-01-29 06:49:05 +08:00
Merge pull request #259 from jrans-va/report-card-cli
Add goreportcard-cli to score repositories locally
This commit is contained in:
48
README.md
48
README.md
@@ -39,6 +39,54 @@ When running the site in a production environment, instead of `make start-dev`,
|
||||
make start
|
||||
```
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
There is also a CLI available for grading applications on your local machine.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
go get github.com/gojp/goreportcard/cmd/goreportcard-cli
|
||||
cd $GOPATH/src/github.com/gojp/goreportcard
|
||||
goreportcard-cli
|
||||
```
|
||||
|
||||
```
|
||||
Grade: A+ (99.9%)
|
||||
Files: 362
|
||||
Issues: 2
|
||||
gofmt: 100%
|
||||
go_vet: 99%
|
||||
gocyclo: 99%
|
||||
golint: 100%
|
||||
ineffassign: 100%
|
||||
license: 100%
|
||||
misspell: 100%
|
||||
```
|
||||
|
||||
Verbose output is also available:
|
||||
```
|
||||
goreportcard-cli -v
|
||||
```
|
||||
|
||||
```
|
||||
Grade: A+ (99.9%)
|
||||
Files: 332
|
||||
Issues: 2
|
||||
gofmt: 100%
|
||||
go_vet: 99%
|
||||
go_vet vendor/github.com/prometheus/client_golang/prometheus/desc.go:25
|
||||
error: cannot find package "github.com/prometheus/client_model/go" in any of: (vet)
|
||||
|
||||
gocyclo: 99%
|
||||
gocyclo download/download.go:22
|
||||
warning: cyclomatic complexity 17 of function download() is high (> 15) (gocyclo)
|
||||
|
||||
golint: 100%
|
||||
ineffassign: 100%
|
||||
license: 100%
|
||||
misspell: 100%
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
Go Report Card is an open source project run by volunteers, and contributions are welcome! Check out the [Issues](https://github.com/gojp/goreportcard/issues) page to see if your idea for a contribution has already been mentioned, and feel free to raise an issue or submit a pull request.
|
||||
|
||||
105
check/check.go
105
check/check.go
@@ -1,5 +1,11 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Check describes what methods various checks (gofmt, go lint, etc.)
|
||||
// should implement
|
||||
type Check interface {
|
||||
@@ -10,3 +16,102 @@ type Check interface {
|
||||
// as well as a map of filename to output
|
||||
Percentage() (float64, []FileSummary, error)
|
||||
}
|
||||
|
||||
// Score represents the result of a single check
|
||||
type Score struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
FileSummaries []FileSummary `json:"file_summaries"`
|
||||
Weight float64 `json:"weight"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// ChecksResult represents the combined result of multiple checks
|
||||
type ChecksResult struct {
|
||||
Checks []Score `json:"checks"`
|
||||
Average float64 `json:"average"`
|
||||
Grade Grade `json:"GradeFromPercentage"`
|
||||
Files int `json:"files"`
|
||||
Issues int `json:"issues"`
|
||||
}
|
||||
|
||||
// Run executes all checks on the given directory
|
||||
func Run(dir string) (ChecksResult, error) {
|
||||
filenames, skipped, err := GoFiles(dir)
|
||||
if err != nil {
|
||||
return ChecksResult{}, fmt.Errorf("could not get filenames: %v", err)
|
||||
}
|
||||
if len(filenames) == 0 {
|
||||
return ChecksResult{}, fmt.Errorf("no .go files found")
|
||||
}
|
||||
|
||||
err = RenameFiles(skipped)
|
||||
if err != nil {
|
||||
log.Println("Could not remove files:", err)
|
||||
}
|
||||
defer RevertFiles(skipped)
|
||||
|
||||
checks := []Check{
|
||||
GoFmt{Dir: dir, Filenames: filenames},
|
||||
GoVet{Dir: dir, Filenames: filenames},
|
||||
GoLint{Dir: dir, Filenames: filenames},
|
||||
GoCyclo{Dir: dir, Filenames: filenames},
|
||||
License{Dir: dir, Filenames: []string{}},
|
||||
Misspell{Dir: dir, Filenames: filenames},
|
||||
IneffAssign{Dir: dir, Filenames: filenames},
|
||||
// ErrCheck{Dir: dir, Filenames: filenames}, // disable errcheck for now, too slow and not finalized
|
||||
}
|
||||
|
||||
ch := make(chan Score)
|
||||
for _, c := range checks {
|
||||
go func(c Check) {
|
||||
p, summaries, err := c.Percentage()
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
log.Printf("ERROR: (%s) %v", c.Name(), err)
|
||||
errMsg = err.Error()
|
||||
}
|
||||
s := Score{
|
||||
Name: c.Name(),
|
||||
Description: c.Description(),
|
||||
FileSummaries: summaries,
|
||||
Weight: c.Weight(),
|
||||
Percentage: p,
|
||||
Error: errMsg,
|
||||
}
|
||||
ch <- s
|
||||
}(c)
|
||||
}
|
||||
|
||||
resp := ChecksResult{
|
||||
Files: len(filenames),
|
||||
}
|
||||
|
||||
var total, totalWeight float64
|
||||
var issues = make(map[string]bool)
|
||||
for i := 0; i < len(checks); i++ {
|
||||
s := <-ch
|
||||
resp.Checks = append(resp.Checks, s)
|
||||
total += s.Percentage * s.Weight
|
||||
totalWeight += s.Weight
|
||||
for _, fs := range s.FileSummaries {
|
||||
issues[fs.Filename] = true
|
||||
}
|
||||
}
|
||||
total /= totalWeight
|
||||
|
||||
sort.Sort(ByWeight(resp.Checks))
|
||||
resp.Average = total
|
||||
resp.Issues = len(issues)
|
||||
resp.Grade = GradeFromPercentage(total * 100)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ByWeight implements sorting for checks by weight descending
|
||||
type ByWeight []Score
|
||||
|
||||
func (a ByWeight) Len() int { return len(a) }
|
||||
func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByWeight) Less(i, j int) bool { return a[i].Weight > a[j].Weight }
|
||||
|
||||
37
check/grade.go
Normal file
37
check/grade.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package check
|
||||
|
||||
// Grade represents a grade returned by the server, which is normally
|
||||
// somewhere between A+ (highest) and F (lowest).
|
||||
type Grade string
|
||||
|
||||
// The Grade constants below indicate the current available
|
||||
// grades.
|
||||
const (
|
||||
GradeAPlus Grade = "A+"
|
||||
GradeA = "A"
|
||||
GradeB = "B"
|
||||
GradeC = "C"
|
||||
GradeD = "D"
|
||||
GradeE = "E"
|
||||
GradeF = "F"
|
||||
)
|
||||
|
||||
// GradeFromPercentage is a helper for getting the GradeFromPercentage for a percentage
|
||||
func GradeFromPercentage(percentage float64) Grade {
|
||||
switch {
|
||||
case percentage > 90:
|
||||
return GradeAPlus
|
||||
case percentage > 80:
|
||||
return GradeA
|
||||
case percentage > 70:
|
||||
return GradeB
|
||||
case percentage > 60:
|
||||
return GradeC
|
||||
case percentage > 50:
|
||||
return GradeD
|
||||
case percentage > 40:
|
||||
return GradeE
|
||||
default:
|
||||
return GradeF
|
||||
}
|
||||
}
|
||||
@@ -263,17 +263,16 @@ func getFileSummaryMap(out *bufio.Scanner, dir string) (map[string]FileSummary,
|
||||
outer:
|
||||
for out.Scan() {
|
||||
filename := strings.Split(out.Text(), ":")[0]
|
||||
filename = strings.TrimPrefix(filename, "_repos/src")
|
||||
for _, skip := range skipSuffixes {
|
||||
if strings.HasSuffix(filename, skip) {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
if autoGenerated("_repos/src" + filename) {
|
||||
if autoGenerated(filename) {
|
||||
continue outer
|
||||
}
|
||||
|
||||
filename = strings.TrimPrefix(filename, "_repos/src")
|
||||
fu := fileURL(dir, filename)
|
||||
fs := fsMap[filename]
|
||||
if fs.Filename == "" {
|
||||
|
||||
38
cmd/goreportcard-cli/main.go
Normal file
38
cmd/goreportcard-cli/main.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/gojp/goreportcard/check"
|
||||
"log"
|
||||
)
|
||||
|
||||
var (
|
||||
dir = flag.String("d", ".", "Root directory of your Go application")
|
||||
verbose = flag.Bool("v", false, "Verbose output")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
result, err := check.Run(*dir)
|
||||
if err != nil {
|
||||
log.Fatalf("Fatal error checking %s: %s", *dir, err.Error())
|
||||
}
|
||||
|
||||
fmt.Printf("Grade: %s (%.1f%%)\n", result.Grade, result.Average*100)
|
||||
fmt.Printf("Files: %d\n", result.Files)
|
||||
fmt.Printf("Issues: %d\n", result.Issues)
|
||||
|
||||
for _, c := range result.Checks {
|
||||
fmt.Printf("%s: %d%%\n", c.Name, int64(c.Percentage*100))
|
||||
if *verbose && len(c.FileSummaries) > 0 {
|
||||
for _, f := range c.FileSummaries {
|
||||
fmt.Printf("\t%s\n", f.Filename)
|
||||
for _, e := range f.Errors {
|
||||
fmt.Printf("\t\tLine %d: %s\n", e.LineNumber, e.ErrorString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,48 +2,13 @@ package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gojp/goreportcard/check"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Grade represents a grade returned by the server, which is normally
|
||||
// somewhere between A+ (highest) and F (lowest).
|
||||
type Grade string
|
||||
|
||||
// The Grade constants below indicate the current available
|
||||
// grades.
|
||||
const (
|
||||
GradeAPlus Grade = "A+"
|
||||
GradeA = "A"
|
||||
GradeB = "B"
|
||||
GradeC = "C"
|
||||
GradeD = "D"
|
||||
GradeE = "E"
|
||||
GradeF = "F"
|
||||
)
|
||||
|
||||
// grade is a helper for getting the grade for a percentage
|
||||
func grade(percentage float64) Grade {
|
||||
switch {
|
||||
case percentage > 90:
|
||||
return GradeAPlus
|
||||
case percentage > 80:
|
||||
return GradeA
|
||||
case percentage > 70:
|
||||
return GradeB
|
||||
case percentage > 60:
|
||||
return GradeC
|
||||
case percentage > 50:
|
||||
return GradeD
|
||||
case percentage > 40:
|
||||
return GradeE
|
||||
default:
|
||||
return GradeF
|
||||
}
|
||||
}
|
||||
|
||||
func badgePath(grade Grade, style string, dev bool) string {
|
||||
func badgePath(grade check.Grade, style string, dev bool) string {
|
||||
return fmt.Sprintf("assets/badges/%s_%s.svg", strings.ToLower(string(grade)), strings.ToLower(style))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gojp/goreportcard/check"
|
||||
"github.com/gojp/goreportcard/download"
|
||||
)
|
||||
@@ -63,26 +62,17 @@ func getFromCache(repo string) (checksResp, error) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type score struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
FileSummaries []check.FileSummary `json:"file_summaries"`
|
||||
Weight float64 `json:"weight"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type checksResp struct {
|
||||
Checks []score `json:"checks"`
|
||||
Average float64 `json:"average"`
|
||||
Grade Grade `json:"grade"`
|
||||
Files int `json:"files"`
|
||||
Issues int `json:"issues"`
|
||||
Repo string `json:"repo"`
|
||||
ResolvedRepo string `json:"resolvedRepo"`
|
||||
LastRefresh time.Time `json:"last_refresh"`
|
||||
LastRefreshFormatted string `json:"formatted_last_refresh"`
|
||||
LastRefreshHumanized string `json:"humanized_last_refresh"`
|
||||
Checks []check.Score `json:"checks"`
|
||||
Average float64 `json:"average"`
|
||||
Grade check.Grade `json:"grade"`
|
||||
Files int `json:"files"`
|
||||
Issues int `json:"issues"`
|
||||
Repo string `json:"repo"`
|
||||
ResolvedRepo string `json:"resolvedRepo"`
|
||||
LastRefresh time.Time `json:"last_refresh"`
|
||||
LastRefreshFormatted string `json:"formatted_last_refresh"`
|
||||
LastRefreshHumanized string `json:"humanized_last_refresh"`
|
||||
}
|
||||
|
||||
func newChecksResp(repo string, forceRefresh bool) (checksResp, error) {
|
||||
@@ -92,7 +82,7 @@ func newChecksResp(repo string, forceRefresh bool) (checksResp, error) {
|
||||
// just log the error and continue
|
||||
log.Println(err)
|
||||
} else {
|
||||
resp.Grade = grade(resp.Average * 100) // grade is not stored for some repos, yet
|
||||
resp.Grade = check.GradeFromPercentage(resp.Average * 100) // grade is not stored for some repos, yet
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
@@ -104,82 +94,25 @@ func newChecksResp(repo string, forceRefresh bool) (checksResp, error) {
|
||||
}
|
||||
|
||||
repo = repoRoot.Root
|
||||
|
||||
dir := dirName(repo)
|
||||
filenames, skipped, err := check.GoFiles(dir)
|
||||
checkResult, err := check.Run(dirName(repo))
|
||||
if err != nil {
|
||||
return checksResp{}, fmt.Errorf("could not get filenames: %v", err)
|
||||
}
|
||||
if len(filenames) == 0 {
|
||||
return checksResp{}, fmt.Errorf("no .go files found")
|
||||
}
|
||||
|
||||
err = check.RenameFiles(skipped)
|
||||
if err != nil {
|
||||
log.Println("Could not remove files:", err)
|
||||
}
|
||||
defer check.RevertFiles(skipped)
|
||||
|
||||
checks := []check.Check{
|
||||
check.GoFmt{Dir: dir, Filenames: filenames},
|
||||
check.GoVet{Dir: dir, Filenames: filenames},
|
||||
check.GoLint{Dir: dir, Filenames: filenames},
|
||||
check.GoCyclo{Dir: dir, Filenames: filenames},
|
||||
check.License{Dir: dir, Filenames: []string{}},
|
||||
check.Misspell{Dir: dir, Filenames: filenames},
|
||||
check.IneffAssign{Dir: dir, Filenames: filenames},
|
||||
// check.ErrCheck{Dir: dir, Filenames: filenames}, // disable errcheck for now, too slow and not finalized
|
||||
}
|
||||
|
||||
ch := make(chan score)
|
||||
for _, c := range checks {
|
||||
go func(c check.Check) {
|
||||
p, summaries, err := c.Percentage()
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
log.Printf("ERROR: (%s) %v", c.Name(), err)
|
||||
errMsg = err.Error()
|
||||
}
|
||||
s := score{
|
||||
Name: c.Name(),
|
||||
Description: c.Description(),
|
||||
FileSummaries: summaries,
|
||||
Weight: c.Weight(),
|
||||
Percentage: p,
|
||||
Error: errMsg,
|
||||
}
|
||||
ch <- s
|
||||
}(c)
|
||||
return checksResp{}, err
|
||||
}
|
||||
|
||||
t := time.Now().UTC()
|
||||
resp := checksResp{
|
||||
Checks: checkResult.Checks,
|
||||
Average: checkResult.Average,
|
||||
Grade: checkResult.Grade,
|
||||
Files: checkResult.Files,
|
||||
Issues: checkResult.Issues,
|
||||
Repo: repo,
|
||||
ResolvedRepo: repoRoot.Repo,
|
||||
Files: len(filenames),
|
||||
LastRefresh: t,
|
||||
LastRefreshFormatted: t.Format(time.UnixDate),
|
||||
LastRefreshHumanized: humanize.Time(t),
|
||||
}
|
||||
|
||||
var total, totalWeight float64
|
||||
var issues = make(map[string]bool)
|
||||
for i := 0; i < len(checks); i++ {
|
||||
s := <-ch
|
||||
resp.Checks = append(resp.Checks, s)
|
||||
total += s.Percentage * s.Weight
|
||||
totalWeight += s.Weight
|
||||
for _, fs := range s.FileSummaries {
|
||||
issues[fs.Filename] = true
|
||||
}
|
||||
}
|
||||
total /= totalWeight
|
||||
|
||||
sort.Sort(ByWeight(resp.Checks))
|
||||
resp.Average = total
|
||||
resp.Issues = len(issues)
|
||||
resp.Grade = grade(total * 100)
|
||||
|
||||
respBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return checksResp{}, fmt.Errorf("could not marshal json: %v", err)
|
||||
@@ -242,10 +175,3 @@ func newChecksResp(repo string, forceRefresh bool) (checksResp, error) {
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ByWeight implements sorting for checks by weight descending
|
||||
type ByWeight []score
|
||||
|
||||
func (a ByWeight) Len() int { return len(a) }
|
||||
func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByWeight) Less(i, j int) bool { return a[i].Weight > a[j].Weight }
|
||||
|
||||
Reference in New Issue
Block a user