blob: 6f9a8f3311fc9e23e5510198e715676117611206 [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The evaluations page, which displays signals for
// package and module quality.
package frontend
import (
"context"
"fmt"
"go/ast"
"strings"
"time"
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/godoc"
"golang.org/x/pkgsite/internal/version"
)
type evalType struct {
Label string // displayed on the page
Description string // displayed from "?" icon
MaxScore int // number of bars
}
var (
licenseEval = &evalType{
Label: "License for this package or module",
Description: `The module license.
0: no license
1: non-redistributable license
2: redistributable license`,
MaxScore: 2,
}
moduleVersionEval = &evalType{
Label: "Module version",
Description: `Version of this module.
0: untagged
1: tagged, unstable (v0)
2: tagged, stable (v1 or higher)`,
MaxScore: 2,
}
)
type eval struct {
Type *evalType
Value string // displayed on the page
Score int // number of colored bars
}
type evalsDetails struct {
Evals []eval
}
func fetchEvalsDetails(ctx context.Context, ds internal.DataSource, um *internal.UnitMeta) (*evalsDetails, error) {
u, err := ds.GetUnit(ctx, um, internal.WithLicenses, internal.BuildContext{})
if err != nil {
return nil, err
}
licEval := eval{Type: licenseEval}
switch {
case len(u.LicenseContents) == 0:
licEval.Score = 0
licEval.Value = "no license"
case !u.IsRedistributable:
licEval.Score = 1
licEval.Value = "non-redistributable license"
default:
licEval.Score = 2
licEval.Value = "redistributable license"
}
modEval := eval{Type: moduleVersionEval}
versionType, err := version.ParseType(um.Version)
if err != nil {
return nil, err
}
switch {
case version.IsPseudo(um.Version) || !semver.IsValid(um.Version):
modEval.Score = 0
modEval.Value = "untagged"
case semver.Major(um.Version) == "v0" || versionType == version.TypePrerelease:
modEval.Score = 1
modEval.Value = "tagged, unstable"
default:
modEval.Score = 2
modEval.Value = "tagged, stable"
}
return &evalsDetails{
Evals: []eval{licEval, modEval},
}, nil
}
const (
day = 24 * time.Hour
month = 31 * day
year = 365 * day
)
// ageString formats a time.Duration into a human-readable age string.
func ageString(dur time.Duration) string {
pluralize := func(n time.Duration, s string) string {
if n == 1 {
return fmt.Sprintf("1 %s", s)
}
return fmt.Sprintf("%d %ss", n, s)
}
if dur < day {
return "less than a day"
}
if dur < month {
return pluralize(dur/day, "day")
}
if dur < year {
monthStr := pluralize(dur/month, "month")
days := (dur % month) / day
if days == 0 {
return monthStr
}
return monthStr + ", " + pluralize(days, "day")
}
yearStr := pluralize(dur/year, "year")
months := (dur % year) / month
if months == 0 {
return yearStr
}
return yearStr + ", " + pluralize(months, "month")
}
// docSummary is a summary of a package's documentation, intended
// for creating an evaluation.
type docSummary struct {
packageHasDoc bool
}
// summarizeDocumentation produces a docSummary for the given package.
func summarizeDocumentation(docPkg *godoc.Package) docSummary {
var summary docSummary
// Empty package? Nothing to do.
if docPkg == nil || len(docPkg.Files) == 0 {
return summary
}
// Collect non-test files.
var files []*ast.File
for _, f := range docPkg.Files {
if f == nil || f.AST == nil {
continue
}
if strings.HasSuffix(f.Name, "_test.go") {
continue
}
files = append(files, f.AST)
}
// No non-test files? Nothing to do.
if len(files) == 0 {
return summary
}
// Main package? Nothing to do (we don't insist
// that mains have doc.)
pkgName := files[0].Name.Name
if pkgName == "main" {
return summary
}
// Is there a package-level doc string?
for _, file := range files {
if file.Doc != nil {
summary.packageHasDoc = true
}
}
// TODO: count symbols with doc.
return summary
}