Files
awesome-docker/internal/linter/fixer.go
T
Julien Bisconti 29222bfcb5
Deploy to GitHub Pages / build (push) Failing after 51s
Deploy to GitHub Pages / deploy (push) Has been skipped
Pull Requests / Weekly QA / test (push) Failing after 1m13s
Broken Links Report / check-links (push) Failing after 45s
feat: add prune subcommand, drop archived/stale entries (#1441)
* 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>
2026-05-18 23:46:32 +02:00

243 lines
5.5 KiB
Markdown

package linter
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
"github.com/veggiemonk/awesome-docker/internal/parser"
)
// attributionRe matches trailing author attributions like:
//
// by [@author](url), by [@author][ref], by @author
//
// Also handles "Created by", "Maintained by" etc.
var attributionRe = regexp.MustCompile(
`\s+(?:(?:[Cc]reated|[Mm]aintained|[Bb]uilt)\s+)?by\s+\[@[^\]]+\](?:\([^)]*\)|\[[^\]]*\])\.?$`,
)
// bareAttributionRe matches: by @author at end of line (no link).
var bareAttributionRe = regexp.MustCompile(`\s+by\s+@\w+\.?$`)
// sectionHeadingRe matches markdown headings.
var sectionHeadingRe = regexp.MustCompile(`^(#{1,6})\s+(.+?)(?:\s*<!--.*-->)?$`)
// RemoveAttribution strips author attribution from a description string.
func RemoveAttribution(desc string) string {
desc = attributionRe.ReplaceAllString(desc, "")
desc = bareAttributionRe.ReplaceAllString(desc, "")
return strings.TrimSpace(desc)
}
// FormatEntry reconstructs a markdown list line from a parsed Entry.
func FormatEntry(e parser.Entry) string {
desc := e.Description
var markers []string
for _, m := range e.Markers {
switch m {
case parser.MarkerAbandoned:
markers = append(markers, ":skull:")
case parser.MarkerPaid:
markers = append(markers, ":yen:")
case parser.MarkerWIP:
markers = append(markers, ":construction:")
case parser.MarkerStale:
markers = append(markers, ":ice_cube:")
}
}
if len(markers) > 0 {
desc = strings.Join(markers, " ") + " " + desc
}
return fmt.Sprintf("- [%s](%s) - %s", e.Name, e.URL, desc)
}
// FixFile reads the README, fixes entries (capitalize, period, remove attribution,
// sort), and writes the result back.
func FixFile(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
return 0, err
}
defer f.Close()
var lines []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return 0, err
}
fixCount := 0
var headingLines []int
for i, line := range lines {
if sectionHeadingRe.MatchString(line) {
headingLines = append(headingLines, i)
}
}
// Process each heading block independently to match linter sort scope.
for i, headingIdx := range headingLines {
start := headingIdx + 1
end := len(lines)
if i+1 < len(headingLines) {
end = headingLines[i+1]
}
var entryPositions []int
var entries []parser.Entry
for lineIdx := start; lineIdx < end; lineIdx++ {
entry, err := parser.ParseEntry(lines[lineIdx], lineIdx+1)
if err != nil {
continue
}
entryPositions = append(entryPositions, lineIdx)
entries = append(entries, entry)
}
if len(entries) == 0 {
continue
}
var fixed []parser.Entry
for _, e := range entries {
f := FixEntry(e)
f.Description = RemoveAttribution(f.Description)
// Re-apply period after removing attribution (it may have been stripped)
if len(f.Description) > 0 && !strings.HasSuffix(f.Description, ".") {
f.Description += "."
}
fixed = append(fixed, f)
}
sorted := SortEntries(fixed)
for j, e := range sorted {
newLine := FormatEntry(e)
lineIdx := entryPositions[j]
if lines[lineIdx] != newLine {
fixCount++
lines[lineIdx] = newLine
}
}
}
if fixCount == 0 {
return 0, nil
}
// Write back
out, err := os.Create(path)
if err != nil {
return 0, err
}
defer out.Close()
w := bufio.NewWriter(out)
for i, line := range lines {
if _, err := w.WriteString(line); err != nil {
return 0, err
}
if i < len(lines)-1 {
if _, err := w.WriteString("\n"); err != nil {
return 0, err
}
}
}
// Preserve trailing newline if original had one
if _, err := w.WriteString("\n"); err != nil {
return 0, err
}
return fixCount, w.Flush()
}
// SortFile reads the README, sorts entries alphabetically within each section,
// and writes the result back. Unlike FixFile, it does not modify descriptions
// (no capitalization, period, or attribution changes).
func SortFile(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
return 0, err
}
defer f.Close()
var lines []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return 0, err
}
fixCount := 0
var headingLines []int
for i, line := range lines {
if sectionHeadingRe.MatchString(line) {
headingLines = append(headingLines, i)
}
}
for i, headingIdx := range headingLines {
start := headingIdx + 1
end := len(lines)
if i+1 < len(headingLines) {
end = headingLines[i+1]
}
var entryPositions []int
var entries []parser.Entry
for lineIdx := start; lineIdx < end; lineIdx++ {
entry, err := parser.ParseEntry(lines[lineIdx], lineIdx+1)
if err != nil {
continue
}
entryPositions = append(entryPositions, lineIdx)
entries = append(entries, entry)
}
if len(entries) == 0 {
continue
}
sorted := SortEntries(entries)
for j, e := range sorted {
lineIdx := entryPositions[j]
// Use the original Raw line from the sorted entry to preserve formatting
if lines[lineIdx] != e.Raw {
fixCount++
lines[lineIdx] = e.Raw
}
}
}
if fixCount == 0 {
return 0, nil
}
out, err := os.Create(path)
if err != nil {
return 0, err
}
defer out.Close()
w := bufio.NewWriter(out)
for i, line := range lines {
if _, err := w.WriteString(line); err != nil {
return 0, err
}
if i < len(lines)-1 {
if _, err := w.WriteString("\n"); err != nil {
return 0, err
}
}
}
if _, err := w.WriteString("\n"); err != nil {
return 0, err
}
return fixCount, w.Flush()
}