a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
159 lines
3.4 KiB
Go
159 lines
3.4 KiB
Go
package audit
|
|
|
|
import "strings"
|
|
|
|
const (
|
|
bloatLineSoft = 600 // > this begins to cost
|
|
bloatLineHard = 1500 // > this is maxed out
|
|
longLineChars = 200
|
|
)
|
|
|
|
// scoreBloat inspects the file's lines and returns a BloatMetrics with a 0-100
|
|
// score. The score is a weighted sum of: file length, long lines, duplicate
|
|
// bullets, and list nesting depth.
|
|
func scoreBloat(lines []string) BloatMetrics {
|
|
m := BloatMetrics{Lines: len(lines)}
|
|
|
|
seenBullets := make(map[string]int)
|
|
inFence := false
|
|
|
|
for _, line := range lines {
|
|
trim := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trim, "```") {
|
|
inFence = !inFence
|
|
m.CodeBlocks++
|
|
continue
|
|
}
|
|
if inFence {
|
|
continue
|
|
}
|
|
|
|
if len(line) > longLineChars {
|
|
m.LongLines++
|
|
}
|
|
|
|
if bulletBody, depth, ok := parseBullet(line); ok {
|
|
m.Bullets++
|
|
if depth > m.MaxDepth {
|
|
m.MaxDepth = depth
|
|
}
|
|
key := strings.ToLower(bulletBody)
|
|
seenBullets[key]++
|
|
}
|
|
}
|
|
|
|
// Code blocks are counted as open+close; collapse to block count.
|
|
m.CodeBlocks = m.CodeBlocks / 2
|
|
|
|
for _, n := range seenBullets {
|
|
if n > 1 {
|
|
m.Duplicates += n - 1
|
|
}
|
|
}
|
|
|
|
m.Score = computeScore(m)
|
|
return m
|
|
}
|
|
|
|
// parseBullet returns (body, indentDepth, true) for markdown list lines.
|
|
func parseBullet(line string) (string, int, bool) {
|
|
leading := 0
|
|
for i := 0; i < len(line); i++ {
|
|
switch line[i] {
|
|
case ' ':
|
|
leading++
|
|
case '\t':
|
|
leading += 4
|
|
default:
|
|
goto done
|
|
}
|
|
}
|
|
done:
|
|
rest := line[min(leading, len(line)):]
|
|
// Handle "- ", "* ", "+ ", and "1. "-style.
|
|
switch {
|
|
case strings.HasPrefix(rest, "- "):
|
|
return strings.TrimSpace(rest[2:]), leading/2 + 1, true
|
|
case strings.HasPrefix(rest, "* "):
|
|
return strings.TrimSpace(rest[2:]), leading/2 + 1, true
|
|
case strings.HasPrefix(rest, "+ "):
|
|
return strings.TrimSpace(rest[2:]), leading/2 + 1, true
|
|
}
|
|
// Numeric: up to 3 digits + ". "
|
|
for i := 0; i < len(rest) && i < 3; i++ {
|
|
if rest[i] < '0' || rest[i] > '9' {
|
|
if i > 0 && strings.HasPrefix(rest[i:], ". ") {
|
|
return strings.TrimSpace(rest[i+2:]), leading/2 + 1, true
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return "", 0, false
|
|
}
|
|
|
|
// computeScore combines the bloat signals into a 0-100 score.
|
|
func computeScore(m BloatMetrics) int {
|
|
score := 0
|
|
|
|
// File length: 0 at <= soft, 50 at >= hard, linear between.
|
|
switch {
|
|
case m.Lines >= bloatLineHard:
|
|
score += 50
|
|
case m.Lines > bloatLineSoft:
|
|
score += (m.Lines - bloatLineSoft) * 50 / (bloatLineHard - bloatLineSoft)
|
|
}
|
|
|
|
// Long lines: up to 20 points when 20%+ of bullets are long.
|
|
if m.Bullets > 0 {
|
|
ratio := float64(m.LongLines) / float64(m.Bullets)
|
|
if ratio > 1 {
|
|
ratio = 1
|
|
}
|
|
score += int(ratio * 20)
|
|
} else if m.LongLines > 5 {
|
|
score += 10
|
|
}
|
|
|
|
// Duplicates: up to 20 points.
|
|
if m.Bullets > 0 {
|
|
dupRatio := float64(m.Duplicates) / float64(m.Bullets)
|
|
if dupRatio > 1 {
|
|
dupRatio = 1
|
|
}
|
|
score += int(dupRatio * 20)
|
|
}
|
|
|
|
// Deep nesting: 10 points if >= 5.
|
|
if m.MaxDepth >= 5 {
|
|
score += 10
|
|
} else if m.MaxDepth >= 4 {
|
|
score += 5
|
|
}
|
|
|
|
if score > 100 {
|
|
score = 100
|
|
}
|
|
return score
|
|
}
|
|
|
|
// aggregateBloat averages the per-file bloat scores, weighting by file length.
|
|
func aggregateBloat(files []FileReport) int {
|
|
if len(files) == 0 {
|
|
return 0
|
|
}
|
|
totalLines := 0
|
|
weighted := 0
|
|
for _, f := range files {
|
|
lines := f.Bloat.Lines
|
|
if lines == 0 {
|
|
lines = 1
|
|
}
|
|
totalLines += lines
|
|
weighted += f.Bloat.Score * lines
|
|
}
|
|
if totalLines == 0 {
|
|
return 0
|
|
}
|
|
return weighted / totalLines
|
|
}
|