Skip to content
This repository has been archived by the owner on Jun 2, 2023. It is now read-only.

Commit

Permalink
gocodescore: implement the first version
Browse files Browse the repository at this point in the history
  • Loading branch information
jirfag committed Oct 22, 2019
1 parent ba7af94 commit b779f54
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ linters:
- scopelint
- dupl
- interfacer
- wsl
- godox
- funlen
- whitespace

# golangci.com configuration
# https://github.com/golangci/golangci/wiki/Configuration
Expand Down
33 changes: 33 additions & 0 deletions cmd/gocodescore/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"encoding/json"
"fmt"
"log"
"os/exec"

"github.com/golangci/golangci-api/internal/api/score"

"github.com/golangci/golangci-lint/pkg/printers"
)

func main() {
cmd := exec.Command("golangci-lint", "run", "--out-format=json", "--issues-exit-code=0")
out, err := cmd.Output()
if err != nil {
log.Fatalf("Failed to run golangci-lint: %s", err)
}

var runRes printers.JSONResult
if err = json.Unmarshal(out, &runRes); err != nil {
log.Fatalf("Failed to json unmarshal golangci-lint output %s: %s", string(out), err)
}

calcRes := score.Calculator{}.Calc(&runRes)
fmt.Printf("Score: %d/%d\n", calcRes.Score, calcRes.MaxScore)
if len(calcRes.Recommendations) != 0 {
for _, rec := range calcRes.Recommendations {
fmt.Printf(" - get %d more score: %s\n", rec.ScoreIncrease, rec.Text)
}
}
}
234 changes: 234 additions & 0 deletions internal/api/score/calculator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package score

import (
"fmt"
"math"
"sort"
"strings"

"github.com/golangci/golangci-lint/pkg/printers"
)

type Calculator struct{}

type Recommendation struct {
Text string
ScoreIncrease int // [0; 100], how much score can be gained if perform recommendation
}

type CalcResult struct {
Score int // [0; 100]
MaxScore int
Recommendations []Recommendation
}

type weightedLinter struct {
name string // linter name
weight float64 // importance of linter
}

func (c Calculator) Calc(runRes *printers.JSONResult) *CalcResult {
const maxScore = 100

if runRes.Report == nil {
return &CalcResult{
Score: maxScore,
MaxScore: maxScore,
}
}

var recomendations []Recommendation
if rec := c.buildRecommendationForDisabledLinters(runRes); rec != nil {
recomendations = append(recomendations, *rec)
}

if rec := c.buildRecommendationForIssues(runRes); rec != nil {
recomendations = append(recomendations, *rec)
}

score := maxScore
for _, rec := range recomendations {
score -= rec.ScoreIncrease
}

return &CalcResult{
Score: score,
MaxScore: maxScore,
Recommendations: recomendations,
}
}

func (c Calculator) buildRecommendationForDisabledLinters(runRes *printers.JSONResult) *Recommendation {
enabledLinters := map[string]bool{}
for _, linter := range runRes.Report.Linters {
if linter.Enabled {
enabledLinters[linter.Name] = true
}
}

linters := c.getNeededLinters(enabledLinters)

var disabledNeededLinters []weightedLinter
for _, wl := range linters {
if !enabledLinters[wl.name] {
disabledNeededLinters = append(disabledNeededLinters, wl)
}
}

if len(disabledNeededLinters) == 0 {
return nil
}

weight := float64(0)
var disabledNeededLinterNames []string
for _, wl := range disabledNeededLinters {
weight += wl.weight
disabledNeededLinterNames = append(disabledNeededLinterNames, wl.name)
}

sort.Strings(disabledNeededLinterNames)

const maxScore = 100
score := int(weight * maxScore)
if score == 0 { // rounded to zero
return nil
}

return &Recommendation{
ScoreIncrease: score,
Text: fmt.Sprintf("enable linters %s", strings.Join(disabledNeededLinterNames, ", ")),
}
}

//nolint:gocyclo
func (c Calculator) buildRecommendationForIssues(runRes *printers.JSONResult) *Recommendation {
enabledLinters := map[string]bool{}
for _, linter := range runRes.Report.Linters {
if linter.Enabled {
enabledLinters[linter.Name] = true
}
}

linters := c.getNeededLinters(enabledLinters)

lintersMap := map[string]*weightedLinter{}
for i := range linters {
lintersMap[linters[i].name] = &linters[i]
}

issuesPerLinter := map[string]int{}
for _, issue := range runRes.Issues {
issuesPerLinter[issue.FromLinter]++
}

if len(issuesPerLinter) == 0 {
return nil
}

weight := float64(0)
for linter, issueCount := range issuesPerLinter {
wl := lintersMap[linter]
if wl == nil {
continue // not needed linter
}

if issueCount > 100 {
issueCount = 100
}

// 100 -> 1, 50 -> 0.85, 10 -> 0.5, 5 -> 0.35, 1 -> 0
normalizedLog := math.Log10(float64(issueCount)) / 2
const minScoreForAnyIssue = 0.2
weight += wl.weight * (minScoreForAnyIssue + (1-minScoreForAnyIssue)*normalizedLog)
}

var neededLintersWithIssues []string
for linter := range issuesPerLinter {
if _, ok := lintersMap[linter]; ok {
neededLintersWithIssues = append(neededLintersWithIssues, linter)
}
}

sort.Strings(neededLintersWithIssues)

const maxScore = 100
score := int(weight * maxScore)
if score == 0 { // rounded to zero
return nil
}

return &Recommendation{
ScoreIncrease: score,
Text: fmt.Sprintf("fix issues from linters %s", strings.Join(neededLintersWithIssues, ", ")),
}
}

func (c Calculator) getNeededLinters(enabledLinters map[string]bool) []weightedLinter {
bugsLinters := c.getNeededBugsLintersWeights()
styleLinters := c.getNeededStyleLintersWeights(enabledLinters)

const bugsWeight = 0.7
var linters []weightedLinter
for _, wl := range bugsLinters {
wl.weight *= bugsWeight
linters = append(linters, wl)
}
for _, wl := range styleLinters {
wl.weight *= 1 - bugsWeight
linters = append(linters, wl)
}

return linters
}

func (c Calculator) normalizeWeightedLinters(linters []weightedLinter) []weightedLinter {
res := make([]weightedLinter, 0, len(linters))
totalWeight := float64(0)
for _, wl := range linters {
totalWeight += wl.weight
}

for _, wl := range linters {
res = append(res, weightedLinter{wl.name, wl.weight / totalWeight})
}

return res
}

func (c Calculator) getNeededBugsLintersWeights() []weightedLinter {
return c.normalizeWeightedLinters([]weightedLinter{
{"govet", 1},
{"staticcheck", 1},
{"errcheck", 0.8},
{"bodyclose", 0.7}, // low because can have false-positives
{"typecheck", 0.5},
})
}

func (c Calculator) getNeededStyleLintersWeights(enabledLinters map[string]bool) []weightedLinter {
linters := []weightedLinter{
{"goimports", 1},
{"dogsled", 0.5},
{"gochecknoglobals", 0.4}, // low because can have false-positives
{"gochecknoinits", 0.4},
{"goconst", 0.3},
{"golint", 1},
{"gosimple", 0.6},
{"lll", 0.1},
{"misspell", 0.4},
{"unconvert", 0.4},
{"ineffassign", 0.5},
}

const (
gocognit = "gocognit"
gocyclo = "gocyclo"
)
complexityLinter := gocognit
if !enabledLinters[gocognit] && enabledLinters[gocyclo] {
complexityLinter = gocyclo
}
linters = append(linters, weightedLinter{complexityLinter, 0.8})

return c.normalizeWeightedLinters(linters)
}

0 comments on commit b779f54

Please sign in to comment.