mirror of
https://github.com/veggiemonk/awesome-docker.git
synced 2026-06-30 18:40:32 +02:00
29222bfcb5
* feat: add prune subcommand, drop archived/stale entries, add container-explorer Add a new `awesome-docker prune` subcommand that removes README entries whose repository health status matches a configurable set (default: archived,stale). URLs are read from the local health cache, or from a markdown report file via --from-report when the cache is outdated. Apply it against the issue #1439 health report to remove 5 entries that survived the recent reorg: stitchocker, docker-consul, blockbridge-docker-volume, docker-explorer, dockdash. Add google/container-explorer in the Security section as the actively maintained successor to the now-archived google/docker-explorer. Co-Authored-By: Claude <noreply@anthropic.com> * golangci-lint config * fix: address golangci-lint findings Fixes errcheck on bufio.Writer.WriteString, gocritic rangeValCopy via indexed loops with pointer locals, gosec G703 on user-supplied CLI output path, noctx by switching to exec.CommandContext with a timeout in the TUI url opener, prealloc in the scorer test, plus fieldalignment struct reorders and golines line breaks from --fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
154 lines
3.7 KiB
Markdown
154 lines
3.7 KiB
Markdown
package linter
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/veggiemonk/awesome-docker/internal/parser"
|
|
)
|
|
|
|
// Rule identifies a linting rule.
|
|
type Rule string
|
|
|
|
const (
|
|
RuleDescriptionCapital Rule = "description-capital"
|
|
RuleDescriptionPeriod Rule = "description-period"
|
|
RuleSorted Rule = "sorted"
|
|
RuleDuplicateURL Rule = "duplicate-url"
|
|
)
|
|
|
|
// Severity of a lint issue.
|
|
type Severity int
|
|
|
|
const (
|
|
SeverityError Severity = iota
|
|
SeverityWarning
|
|
)
|
|
|
|
// Issue is a single lint problem found.
|
|
type Issue struct {
|
|
Rule Rule
|
|
Message string
|
|
Severity Severity
|
|
Line int
|
|
}
|
|
|
|
func (i Issue) String() string {
|
|
sev := "ERROR"
|
|
if i.Severity == SeverityWarning {
|
|
sev = "WARN"
|
|
}
|
|
return fmt.Sprintf("[%s] line %d: %s (%s)", sev, i.Line, i.Message, i.Rule)
|
|
}
|
|
|
|
// CheckEntry validates a single entry against formatting rules.
|
|
func CheckEntry(e parser.Entry) []Issue {
|
|
var issues []Issue
|
|
|
|
if first, ok := firstLetter(e.Description); ok && !unicode.IsUpper(first) {
|
|
issues = append(issues, Issue{
|
|
Rule: RuleDescriptionCapital,
|
|
Severity: SeverityError,
|
|
Line: e.Line,
|
|
Message: fmt.Sprintf("%q: description should start with a capital letter", e.Name),
|
|
})
|
|
}
|
|
|
|
if len(e.Description) > 0 && !strings.HasSuffix(e.Description, ".") {
|
|
issues = append(issues, Issue{
|
|
Rule: RuleDescriptionPeriod,
|
|
Severity: SeverityError,
|
|
Line: e.Line,
|
|
Message: fmt.Sprintf("%q: description should end with a period", e.Name),
|
|
})
|
|
}
|
|
|
|
return issues
|
|
}
|
|
|
|
// CheckSorted verifies entries are in alphabetical order (case-insensitive).
|
|
func CheckSorted(entries []parser.Entry) []Issue {
|
|
var issues []Issue
|
|
for i := 1; i < len(entries); i++ {
|
|
prev := strings.ToLower(entries[i-1].Name)
|
|
curr := strings.ToLower(entries[i].Name)
|
|
if prev > curr {
|
|
issues = append(issues, Issue{
|
|
Rule: RuleSorted,
|
|
Severity: SeverityError,
|
|
Line: entries[i].Line,
|
|
Message: fmt.Sprintf(
|
|
"%q should come before %q (alphabetical order)",
|
|
entries[i].Name,
|
|
entries[i-1].Name,
|
|
),
|
|
})
|
|
}
|
|
}
|
|
return issues
|
|
}
|
|
|
|
// CheckDuplicates finds entries with the same URL across the entire document.
|
|
func CheckDuplicates(entries []parser.Entry) []Issue {
|
|
var issues []Issue
|
|
seen := make(map[string]int) // URL -> first line number
|
|
for _, e := range entries {
|
|
url := strings.TrimRight(e.URL, "/")
|
|
if firstLine, exists := seen[url]; exists {
|
|
issues = append(issues, Issue{
|
|
Rule: RuleDuplicateURL,
|
|
Severity: SeverityError,
|
|
Line: e.Line,
|
|
Message: fmt.Sprintf("duplicate URL %q (first seen at line %d)", e.URL, firstLine),
|
|
})
|
|
} else {
|
|
seen[url] = e.Line
|
|
}
|
|
}
|
|
return issues
|
|
}
|
|
|
|
// firstLetter returns the first unicode letter in s and true, or zero and false if none.
|
|
func firstLetter(s string) (rune, bool) {
|
|
for _, r := range s {
|
|
if unicode.IsLetter(r) {
|
|
return r, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// FixEntry returns a copy of the entry with auto-fixable issues corrected.
|
|
func FixEntry(e parser.Entry) parser.Entry {
|
|
fixed := e
|
|
if len(fixed.Description) > 0 {
|
|
// Capitalize first letter (find it, may not be at index 0)
|
|
runes := []rune(fixed.Description)
|
|
for i, r := range runes {
|
|
if unicode.IsLetter(r) {
|
|
runes[i] = unicode.ToUpper(r)
|
|
break
|
|
}
|
|
}
|
|
fixed.Description = string(runes)
|
|
|
|
// Ensure period at end
|
|
if !strings.HasSuffix(fixed.Description, ".") {
|
|
fixed.Description += "."
|
|
}
|
|
}
|
|
return fixed
|
|
}
|
|
|
|
// SortEntries returns a sorted copy of entries (case-insensitive by Name).
|
|
func SortEntries(entries []parser.Entry) []parser.Entry {
|
|
sorted := make([]parser.Entry, len(entries))
|
|
copy(sorted, entries)
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
return strings.ToLower(sorted[i].Name) < strings.ToLower(sorted[j].Name)
|
|
})
|
|
return sorted
|
|
}
|