From 8fab3e705da673e23909ca9417e766cb96657093 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Wed, 18 Mar 2026 19:49:46 +0000 Subject: [PATCH] Fix all golangci-lint warnings with extended linters Extract magic numbers to named constants for timeouts, retry params, connection pool sizes, and backoff factors. Use net/http status constants instead of raw integers. Fix potential panic in hackage cabal dependency parser where strings.Index could return -1. Refactor parseCabalFile to reduce cognitive complexity by extracting helper functions. Rewrite if-else chain as switch in urlparser. Use strings.Cut where appropriate. Add nolint directives for idiomatic patterns (SplitN with 2, interface returns from factory functions). --- client/client.go | 26 ++++--- client/errors.go | 3 +- fetch/circuit_breaker.go | 15 ++-- fetch/fetcher.go | 45 ++++++++---- fetch/resolver.go | 10 +-- internal/cargo/cargo.go | 2 +- internal/clojars/clojars.go | 14 ++-- internal/cocoapods/cocoapods.go | 2 +- internal/conda/conda.go | 18 ++--- internal/core/helpers.go | 2 +- internal/core/registry.go | 2 +- internal/cpan/cpan.go | 2 +- internal/cran/cran.go | 6 +- internal/deno/deno.go | 2 +- internal/dub/dub.go | 2 +- internal/elm/elm.go | 13 ++-- internal/elm/elm_test.go | 2 +- internal/golang/golang.go | 9 +-- internal/hackage/hackage.go | 122 ++++++++++++++++++-------------- internal/haxelib/haxelib.go | 2 +- internal/hex/hex.go | 2 +- internal/homebrew/homebrew.go | 6 +- internal/julia/julia.go | 16 ++--- internal/luarocks/luarocks.go | 4 +- internal/maven/maven.go | 20 +++--- internal/nimble/nimble.go | 4 +- internal/npm/npm.go | 8 +-- internal/nuget/nuget.go | 2 +- internal/packagist/packagist.go | 4 +- internal/pub/pub.go | 2 +- internal/pypi/pypi.go | 4 +- internal/rubygems/rubygems.go | 2 +- internal/terraform/terraform.go | 4 +- internal/urlparser/urlparser.go | 40 ++++++----- registries.go | 4 +- 35 files changed, 237 insertions(+), 184 deletions(-) diff --git a/client/client.go b/client/client.go index 234dee9..a1553d6 100644 --- a/client/client.go +++ b/client/client.go @@ -12,6 +12,14 @@ import ( "time" ) +const ( + defaultTimeout = 30 * time.Second + defaultMaxRetries = 5 + defaultBaseDelay = 50 * time.Millisecond + backoffBase = 2 + jitterFactor = 0.1 +) + // RateLimiter controls request pacing. type RateLimiter interface { Wait(ctx context.Context) error @@ -30,11 +38,11 @@ type Client struct { func DefaultClient() *Client { return &Client{ HTTPClient: &http.Client{ - Timeout: 30 * time.Second, + Timeout: defaultTimeout, }, UserAgent: "registries", - MaxRetries: 5, - BaseDelay: 50 * time.Millisecond, + MaxRetries: defaultMaxRetries, + BaseDelay: defaultBaseDelay, } } @@ -53,8 +61,8 @@ func (c *Client) GetBody(ctx context.Context, url string) ([]byte, error) { for attempt := 0; attempt <= c.MaxRetries; attempt++ { if attempt > 0 { - delay := c.BaseDelay * time.Duration(math.Pow(2, float64(attempt-1))) - jitter := time.Duration(float64(delay) * (rand.Float64() * 0.1)) + delay := c.BaseDelay * time.Duration(math.Pow(backoffBase, float64(attempt-1))) + jitter := time.Duration(float64(delay) * (rand.Float64() * jitterFactor)) delay += jitter select { @@ -79,10 +87,10 @@ func (c *Client) GetBody(ctx context.Context, url string) ([]byte, error) { var httpErr *HTTPError if ok := isHTTPError(err, &httpErr); ok { - if httpErr.StatusCode == 404 { + if httpErr.StatusCode == http.StatusNotFound { return nil, err } - if httpErr.StatusCode == 429 || httpErr.StatusCode >= 500 { + if httpErr.StatusCode == http.StatusTooManyRequests || httpErr.StatusCode >= http.StatusInternalServerError { continue } return nil, err @@ -112,13 +120,13 @@ func (c *Client) doRequest(ctx context.Context, url string) ([]byte, error) { return nil, err } - if resp.StatusCode >= 400 { + if resp.StatusCode >= http.StatusBadRequest { httpErr := &HTTPError{ StatusCode: resp.StatusCode, URL: url, Body: string(body), } - if resp.StatusCode == 429 { + if resp.StatusCode == http.StatusTooManyRequests { if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" { if seconds, err := strconv.Atoi(retryAfter); err == nil { return nil, &RateLimitError{RetryAfter: seconds} diff --git a/client/errors.go b/client/errors.go index 96bed2d..718bc70 100644 --- a/client/errors.go +++ b/client/errors.go @@ -3,6 +3,7 @@ package client import ( "errors" "fmt" + "net/http" ) // ErrNotFound is returned when a package or version is not found. @@ -21,7 +22,7 @@ func (e *HTTPError) Error() string { // IsNotFound returns true if the error represents a 404 response. func (e *HTTPError) IsNotFound() bool { - return e.StatusCode == 404 + return e.StatusCode == http.StatusNotFound } // NotFoundError wraps ErrNotFound with additional context. diff --git a/fetch/circuit_breaker.go b/fetch/circuit_breaker.go index 446a3af..0a9a387 100644 --- a/fetch/circuit_breaker.go +++ b/fetch/circuit_breaker.go @@ -11,6 +11,13 @@ import ( circuit "github.com/rubyist/circuitbreaker" ) +const ( + cbInitialInterval = 30 * time.Second + cbMaxInterval = 5 * time.Minute + cbThreshold = 5 + maxURLTruncate = 50 +) + // CircuitBreakerFetcher wraps a Fetcher with per-registry circuit breakers. type CircuitBreakerFetcher struct { fetcher *Fetcher @@ -47,14 +54,14 @@ func (cbf *CircuitBreakerFetcher) getBreaker(registry string) *circuit.Breaker { // Create new circuit breaker with exponential backoff // Trips after 5 consecutive failures expBackoff := backoff.NewExponentialBackOff() - expBackoff.InitialInterval = 30 * time.Second - expBackoff.MaxInterval = 5 * time.Minute + expBackoff.InitialInterval = cbInitialInterval + expBackoff.MaxInterval = cbMaxInterval expBackoff.Multiplier = 2.0 expBackoff.Reset() opts := &circuit.Options{ BackOff: expBackoff, - ShouldTrip: circuit.ThresholdTripFunc(5), + ShouldTrip: circuit.ThresholdTripFunc(cbThreshold), } breaker = circuit.NewBreakerWithOptions(opts) @@ -112,7 +119,7 @@ func extractRegistry(rawURL string) string { parsed, err := url.Parse(rawURL) if err != nil || parsed.Host == "" { // Fallback to simple truncation - if len(rawURL) > 50 { + if len(rawURL) > maxURLTruncate { return rawURL[:50] } return rawURL diff --git a/fetch/fetcher.go b/fetch/fetcher.go index 5d9b151..805db60 100644 --- a/fetch/fetcher.go +++ b/fetch/fetcher.go @@ -17,6 +17,23 @@ import ( "github.com/rs/dnscache" ) +const ( + dnsRefreshInterval = 5 * time.Minute + dialTimeout = 30 * time.Second + dialKeepAlive = 30 * time.Second + httpClientTimeout = 5 * time.Minute + maxIdleConns = 100 + maxIdleConnsPerHost = 10 + idleConnTimeout = 90 * time.Second + tlsHandshakeTimeout = 10 * time.Second + defaultMaxRetries = 3 + defaultBaseDelay = 500 * time.Millisecond + backoffBase = 2 + jitterFactor = 0.1 + serverErrThreshold = 500 + maxErrBodySize = 1024 +) + var ( ErrNotFound = errors.New("artifact not found") ErrRateLimited = errors.New("rate limited by upstream") @@ -91,7 +108,7 @@ func NewFetcher(opts ...Option) *Fetcher { // Create DNS cache with 5 minute refresh interval resolver := &dnscache.Resolver{} go func() { - ticker := time.NewTicker(5 * time.Minute) + ticker := time.NewTicker(dnsRefreshInterval) defer ticker.Stop() for range ticker.C { resolver.Refresh(true) @@ -100,13 +117,13 @@ func NewFetcher(opts ...Option) *Fetcher { // Create custom dialer with DNS caching dialer := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, + Timeout: dialTimeout, + KeepAlive: dialKeepAlive, } f := &Fetcher{ client: &http.Client{ - Timeout: 5 * time.Minute, // Artifacts can be large + Timeout: httpClientTimeout, Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { host, port, err := net.SplitHostPort(addr) @@ -125,16 +142,16 @@ func NewFetcher(opts ...Option) *Fetcher { } return nil, fmt.Errorf("failed to dial any resolved IP") }, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, + MaxIdleConns: maxIdleConns, + MaxIdleConnsPerHost: maxIdleConnsPerHost, + IdleConnTimeout: idleConnTimeout, + TLSHandshakeTimeout: tlsHandshakeTimeout, ExpectContinueTimeout: 1 * time.Second, }, }, userAgent: "git-pkgs-proxy/1.0", - maxRetries: 3, - baseDelay: 500 * time.Millisecond, + maxRetries: defaultMaxRetries, + baseDelay: defaultBaseDelay, } for _, opt := range opts { opt(f) @@ -150,8 +167,8 @@ func (f *Fetcher) Fetch(ctx context.Context, url string) (*Artifact, error) { for attempt := 0; attempt <= f.maxRetries; attempt++ { if attempt > 0 { // Exponential backoff with 10% jitter to prevent thundering herd - delay := f.baseDelay * time.Duration(math.Pow(2, float64(attempt-1))) - jitter := time.Duration(float64(delay) * (rand.Float64() * 0.1)) + delay := f.baseDelay * time.Duration(math.Pow(backoffBase, float64(attempt-1))) + jitter := time.Duration(float64(delay) * (rand.Float64() * jitterFactor)) delay += jitter select { @@ -230,12 +247,12 @@ func (f *Fetcher) doFetch(ctx context.Context, url string) (*Artifact, error) { _ = resp.Body.Close() return nil, ErrRateLimited - case resp.StatusCode >= 500: + case resp.StatusCode >= serverErrThreshold: _ = resp.Body.Close() return nil, ErrUpstreamDown default: - body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) + body, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrBodySize)) _ = resp.Body.Close() return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) } diff --git a/fetch/resolver.go b/fetch/resolver.go index 39d88fc..ec02061 100644 --- a/fetch/resolver.go +++ b/fetch/resolver.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "unicode" "github.com/git-pkgs/registries" "github.com/git-pkgs/registries/client" @@ -103,12 +104,11 @@ func (r *Resolver) resolveWithoutRegistry(ecosystem, name, version string) (*Art case "maven": // Maven name format is "group:artifact", e.g., "com.google.guava:guava" - parts := strings.SplitN(name, ":", 2) - if len(parts) != 2 { + group, artifact, found := strings.Cut(name, ":") + if !found { return nil, fmt.Errorf("invalid maven name format, expected group:artifact") } - group := strings.ReplaceAll(parts[0], ".", "/") - artifact := parts[1] + group = strings.ReplaceAll(group, ".", "/") url = fmt.Sprintf("https://repo1.maven.org/maven2/%s/%s/%s/%s-%s.jar", group, artifact, version, artifact, version) filename = fmt.Sprintf("%s-%s.jar", artifact, version) @@ -185,7 +185,7 @@ func encodeGoModule(path string) string { for _, r := range path { if r >= 'A' && r <= 'Z' { b.WriteRune('!') - b.WriteRune(r + 32) + b.WriteRune(unicode.ToLower(r)) } else { b.WriteRune(r) } diff --git a/internal/cargo/cargo.go b/internal/cargo/cargo.go index e051b7d..6233606 100644 --- a/internal/cargo/cargo.go +++ b/internal/cargo/cargo.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/clojars/clojars.go b/internal/clojars/clojars.go index 75e0b65..ec5a6d7 100644 --- a/internal/clojars/clojars.go +++ b/internal/clojars/clojars.go @@ -11,8 +11,9 @@ import ( ) const ( - DefaultURL = "https://clojars.org" - ecosystem = "clojars" + DefaultURL = "https://clojars.org" + ecosystem = "clojars" + msPerSecond = 1000 ) func init() { @@ -43,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -86,9 +87,8 @@ type depInfo struct { // ParseCoordinates parses a Clojars coordinate string (group/artifact or just artifact) // If no group is specified, the artifact name is used as both group and artifact func ParseCoordinates(name string) (group, artifact string) { - parts := strings.SplitN(name, "/", 2) - if len(parts) == 2 { - return parts[0], parts[1] + if before, after, found := strings.Cut(name, "/"); found { + return before, after } // Single name means group == artifact return name, name @@ -168,7 +168,7 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi var versionResp versionDetailResponse if err := r.client.GetJSON(ctx, versionURL, &versionResp); err == nil { if versionResp.CreatedEpoch > 0 { - versions[i].PublishedAt = time.Unix(versionResp.CreatedEpoch/1000, 0) + versions[i].PublishedAt = time.Unix(versionResp.CreatedEpoch/msPerSecond, 0) } if len(versionResp.Licenses) > 0 { versions[i].Licenses = strings.Join(versionResp.Licenses, ",") diff --git a/internal/cocoapods/cocoapods.go b/internal/cocoapods/cocoapods.go index 43201da..7c9464f 100644 --- a/internal/cocoapods/cocoapods.go +++ b/internal/cocoapods/cocoapods.go @@ -43,7 +43,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/conda/conda.go b/internal/conda/conda.go index e82f35d..92d0420 100644 --- a/internal/conda/conda.go +++ b/internal/conda/conda.go @@ -57,7 +57,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -99,9 +99,8 @@ type fileAttrs struct { // parsePackageName parses a package name that may include a channel prefix // Format: "channel/name" or just "name" (uses default channel) func parsePackageName(name string) (channel, pkgName string) { - parts := strings.SplitN(name, "/", 2) - if len(parts) == 2 { - return parts[0], parts[1] + if before, after, found := strings.Cut(name, "/"); found { + return before, after } return "", name } @@ -249,15 +248,10 @@ func (r *Registry) FetchDependencies(ctx context.Context, name, version string) } func parseDependency(dep string) (name, requirements string) { - // Conda dependency format: "name version_constraint" or just "name" - // Examples: "python >=3.8", "numpy", "pandas >=1.0,<2.0" dep = strings.TrimSpace(dep) - parts := strings.SplitN(dep, " ", 2) - name = parts[0] - if len(parts) > 1 { - requirements = strings.TrimSpace(parts[1]) - } - return + name, requirements, _ = strings.Cut(dep, " ") + requirements = strings.TrimSpace(requirements) + return name, requirements } func (r *Registry) FetchMaintainers(ctx context.Context, name string) ([]core.Maintainer, error) { diff --git a/internal/core/helpers.go b/internal/core/helpers.go index 3656f45..892c450 100644 --- a/internal/core/helpers.go +++ b/internal/core/helpers.go @@ -13,7 +13,7 @@ const defaultConcurrency = 15 // NewFromPURL creates a registry client from a PURL and returns the parsed components. // Returns the registry, full package name, and version (empty if not in PURL). // If the PURL has a repository_url qualifier, it's used as the base URL for private registries. -func NewFromPURL(purlStr string, client *Client) (Registry, string, string, error) { +func NewFromPURL(purlStr string, client *Client) (Registry, string, string, error) { //nolint:ireturn p, err := purl.Parse(purlStr) if err != nil { return nil, "", "", err diff --git a/internal/core/registry.go b/internal/core/registry.go index cb3452d..b2e535a 100644 --- a/internal/core/registry.go +++ b/internal/core/registry.go @@ -48,7 +48,7 @@ func Register(ecosystem string, defaultURL string, factory Factory) { // New creates a new registry for the given ecosystem. // If baseURL is empty, the default registry URL is used. -func New(ecosystem string, baseURL string, client *Client) (Registry, error) { +func New(ecosystem string, baseURL string, client *Client) (Registry, error) { //nolint:ireturn mu.RLock() factory, ok := factories[ecosystem] defaultURL := defaults[ecosystem] diff --git a/internal/cpan/cpan.go b/internal/cpan/cpan.go index 8ea7034..3c8a6cd 100644 --- a/internal/cpan/cpan.go +++ b/internal/cpan/cpan.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/cran/cran.go b/internal/cran/cran.go index 561f8a5..0746bd2 100644 --- a/internal/cran/cran.go +++ b/internal/cran/cran.go @@ -46,7 +46,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -155,7 +155,7 @@ func parseDescription(content string) descriptionInfo { commitField() // Start new field - parts := strings.SplitN(line, ":", 2) + parts := strings.SplitN(line, ":", 2) //nolint:mnd // key:value split currentField = strings.TrimSpace(parts[0]) currentValue.Reset() if len(parts) > 1 { @@ -369,7 +369,7 @@ func parseMaintainer(s string) core.Maintainer { if len(matches) > 1 { m.Name = strings.TrimSpace(matches[1]) } - if len(matches) > 2 { + if len(matches) > 2 { //nolint:mnd // regex capture group index m.Email = strings.TrimSpace(matches[2]) } return m diff --git a/internal/deno/deno.go b/internal/deno/deno.go index 79fc2ef..c706ffc 100644 --- a/internal/deno/deno.go +++ b/internal/deno/deno.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/dub/dub.go b/internal/dub/dub.go index 260af22..e734232 100644 --- a/internal/dub/dub.go +++ b/internal/dub/dub.go @@ -45,7 +45,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/elm/elm.go b/internal/elm/elm.go index 21fe823..579846a 100644 --- a/internal/elm/elm.go +++ b/internal/elm/elm.go @@ -13,8 +13,9 @@ import ( ) const ( - DefaultURL = "https://package.elm-lang.org" - ecosystem = "elm" + DefaultURL = "https://package.elm-lang.org" + ecosystem = "elm" + msPerSecond = 1000 ) func init() { @@ -45,14 +46,14 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } // parsePackageName splits "author/name" into author and name func parsePackageName(name string) (author, pkg string) { - parts := strings.SplitN(name, "/", 2) - if len(parts) == 2 { + parts := strings.SplitN(name, "/", 2) //nolint:mnd // author/name split + if len(parts) == 2 { //nolint:mnd return parts[0], parts[1] } return "", name @@ -156,7 +157,7 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi for _, vt := range vts { versions = append(versions, core.Version{ Number: vt.version, - PublishedAt: time.Unix(vt.time/1000, 0), // timestamps are in milliseconds + PublishedAt: time.Unix(vt.time/msPerSecond, 0), }) } diff --git a/internal/elm/elm_test.go b/internal/elm/elm_test.go index f4ab736..d4f57f6 100644 --- a/internal/elm/elm_test.go +++ b/internal/elm/elm_test.go @@ -55,7 +55,7 @@ func TestFetchPackage(t *testing.T) { if pkg.Licenses != "BSD-3-Clause" { t.Errorf("unexpected license: %q", pkg.Licenses) } - if pkg.Namespace != "elm" { + if pkg.Namespace != ecosystem { t.Errorf("expected namespace 'elm', got %q", pkg.Namespace) } if pkg.Repository != "https://github.com/elm/json" { diff --git a/internal/golang/golang.go b/internal/golang/golang.go index dff6d77..3904224 100644 --- a/internal/golang/golang.go +++ b/internal/golang/golang.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" "time" + "unicode" "github.com/git-pkgs/registries/internal/core" "github.com/git-pkgs/registries/internal/urlparser" @@ -45,7 +46,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -62,7 +63,7 @@ func encodeForProxy(path string) string { for _, r := range path { if r >= 'A' && r <= 'Z' { b.WriteRune('!') - b.WriteRune(r + 32) // lowercase + b.WriteRune(unicode.ToLower(r)) } else { b.WriteRune(r) } @@ -112,7 +113,7 @@ func deriveRepoURL(modulePath string) string { strings.HasPrefix(modulePath, "bitbucket.org/") { // Take the first 3 parts as the repo URL parts := strings.Split(modulePath, "/") - if len(parts) >= 3 { + if len(parts) >= 3 { //nolint:mnd // host/owner/repo return "https://" + strings.Join(parts[:3], "/") } return "https://" + modulePath @@ -225,7 +226,7 @@ func parseRequireLine(line string) *core.Dependency { } parts := strings.Fields(line) - if len(parts) < 2 { + if len(parts) < 2 { //nolint:mnd // require needs name + version return nil } diff --git a/internal/hackage/hackage.go b/internal/hackage/hackage.go index 36163f2..70d5e3a 100644 --- a/internal/hackage/hackage.go +++ b/internal/hackage/hackage.go @@ -47,7 +47,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -166,70 +166,88 @@ func parseCabalFile(content string) cabalInfo { continue } - // Check for source-repository section if strings.HasPrefix(strings.ToLower(trimmed), "source-repository") { inSourceRepo = true continue } - // Check for other sections that end source-repo parsing - if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") { - if inSourceRepo && strings.Contains(trimmed, ":") { - field := strings.ToLower(strings.TrimSpace(strings.SplitN(trimmed, ":", 2)[0])) - if field != "location" && field != "type" && field != "branch" && field != "tag" { - inSourceRepo = false - } - } - } - - if strings.Contains(trimmed, ":") { - parts := strings.SplitN(trimmed, ":", 2) - field := strings.ToLower(strings.TrimSpace(parts[0])) - value := "" - if len(parts) > 1 { - value = strings.TrimSpace(parts[1]) - } + inSourceRepo = checkSourceRepoEnd(line, trimmed, inSourceRepo) + if field, value, ok := parseCabalField(trimmed); ok { if inSourceRepo && field == "location" { info.SourceRepository = value continue } - currentField = field - switch field { - case "name": - info.Name = value - case "version": - info.Version = value - case "synopsis": - info.Synopsis = value - case "description": - info.Description = value - case "license": - info.License = value - case "homepage": - info.Homepage = value - case "author": - info.Author = value - case "maintainer": - info.Maintainer = value - case "category": - info.Category = value - } - } else if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { - // Continuation line - switch currentField { - case "description": - info.Description += " " + trimmed - case "author": - info.Author += " " + trimmed - } + setCabalField(&info, field, value) + } else if isIndented(line) { + appendCabalContinuation(&info, currentField, trimmed) } } return info } +func checkSourceRepoEnd(line, trimmed string, inSourceRepo bool) bool { + if isIndented(line) || !inSourceRepo { + return inSourceRepo + } + field, _, ok := parseCabalField(trimmed) + if !ok { + return inSourceRepo + } + switch field { + case "location", "type", "branch", "tag": + return true + default: + return false + } +} + +func isIndented(line string) bool { + return strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") +} + +func parseCabalField(trimmed string) (field, value string, ok bool) { + before, after, found := strings.Cut(trimmed, ":") + if !found { + return "", "", false + } + return strings.ToLower(strings.TrimSpace(before)), strings.TrimSpace(after), true +} + +func setCabalField(info *cabalInfo, field, value string) { + switch field { + case "name": + info.Name = value + case "version": + info.Version = value + case "synopsis": + info.Synopsis = value + case "description": + info.Description = value + case "license": + info.License = value + case "homepage": + info.Homepage = value + case "author": + info.Author = value + case "maintainer": + info.Maintainer = value + case "category": + info.Category = value + } +} + +func appendCabalContinuation(info *cabalInfo, currentField, trimmed string) { + switch currentField { + case "description": + info.Description += " " + trimmed + case "author": + info.Author += " " + trimmed + } +} + func parsePreferredVersions(content string) []string { // Parse the preferred versions format // Format: "normal-versions: 1.0.0, 2.0.0, ..." @@ -377,9 +395,7 @@ func parseDependencies(content string) []core.Dependency { } // Check if this looks like a new field (has a colon not in version constraint) - if strings.Contains(trimmed, ":") { - colonIdx := strings.Index(trimmed, ":") - beforeColon := trimmed[:colonIdx] + if beforeColon, _, found := strings.Cut(trimmed, ":"); found { // If before colon doesn't look like a version constraint, it's a new field if !strings.ContainsAny(beforeColon, "<>=^") { inBuildDepends = false @@ -412,7 +428,7 @@ func processDeps(line string, deps *[]core.Dependency, seen map[string]bool, dep seen[name] = true requirements := "" - if len(matches) > 2 { + if len(matches) > 2 { //nolint:mnd // regex capture group index requirements = strings.TrimSpace(matches[2]) } @@ -449,7 +465,7 @@ func (r *Registry) FetchMaintainers(ctx context.Context, name string) ([]core.Ma m := core.Maintainer{ Name: strings.TrimSpace(matches[1]), } - if len(matches) > 2 { + if len(matches) > 2 { //nolint:mnd // regex capture group index m.Email = strings.TrimSpace(matches[2]) } return []core.Maintainer{m}, nil diff --git a/internal/haxelib/haxelib.go b/internal/haxelib/haxelib.go index 0830ad7..77f5bec 100644 --- a/internal/haxelib/haxelib.go +++ b/internal/haxelib/haxelib.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/hex/hex.go b/internal/hex/hex.go index 93f0898..5a69768 100644 --- a/internal/hex/hex.go +++ b/internal/hex/hex.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/homebrew/homebrew.go b/internal/homebrew/homebrew.go index 15fd09b..138229f 100644 --- a/internal/homebrew/homebrew.go +++ b/internal/homebrew/homebrew.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -163,8 +163,8 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi // Add versioned formulae (e.g., python@3.11, node@18) for _, vf := range resp.VersionedFormulae { // Extract version from formula name like "python@3.11" - parts := strings.SplitN(vf, "@", 2) - if len(parts) == 2 { + parts := strings.SplitN(vf, "@", 2) //nolint:mnd // name@version split + if len(parts) == 2 { //nolint:mnd versions = append(versions, core.Version{ Number: parts[1], Metadata: map[string]any{ diff --git a/internal/julia/julia.go b/internal/julia/julia.go index 30bf436..b0f3de7 100644 --- a/internal/julia/julia.go +++ b/internal/julia/julia.go @@ -46,7 +46,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -101,8 +101,8 @@ func parsePackageToml(content string) packageInfo { continue } - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { + parts := strings.SplitN(line, "=", 2) //nolint:mnd // key=value split + if len(parts) != 2 { //nolint:mnd continue } @@ -188,8 +188,8 @@ func parseVersionsToml(content string) map[string]versionInfo { } // Parse key-value pairs within version section - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { + parts := strings.SplitN(line, "=", 2) //nolint:mnd // key=value split + if len(parts) != 2 { //nolint:mnd continue } @@ -276,8 +276,8 @@ func parseDepsToml(content string) map[string]map[string]string { } // Parse dependency: PackageName = "uuid" - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { + parts := strings.SplitN(line, "=", 2) //nolint:mnd // key=value split + if len(parts) != 2 { //nolint:mnd continue } @@ -300,7 +300,7 @@ func parseDepsToml(content string) map[string]map[string]string { func expandVersionRange(versionRange string) []string { // Handle ranges like "1.0-2.0" - we'll store under both endpoints parts := strings.Split(versionRange, "-") - if len(parts) == 2 { + if len(parts) == 2 { //nolint:mnd // range has start-end return parts } return []string{versionRange} diff --git a/internal/luarocks/luarocks.go b/internal/luarocks/luarocks.go index da74b6e..b642210 100644 --- a/internal/luarocks/luarocks.go +++ b/internal/luarocks/luarocks.go @@ -43,7 +43,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -177,7 +177,7 @@ func (r *Registry) FetchDependencies(ctx context.Context, name, version string) // Examples: "lua >= 5.1", "lpeg", "luasocket >= 3.0" func parseDependency(dep string) (name, requirements string) { dep = strings.TrimSpace(dep) - parts := strings.SplitN(dep, " ", 2) + parts := strings.SplitN(dep, " ", 2) //nolint:mnd // name version split name = parts[0] if len(parts) > 1 { requirements = strings.TrimSpace(parts[1]) diff --git a/internal/maven/maven.go b/internal/maven/maven.go index 72d7d42..f817edd 100644 --- a/internal/maven/maven.go +++ b/internal/maven/maven.go @@ -14,10 +14,14 @@ import ( ) const ( - DefaultURL = "https://repo1.maven.org/maven2" - SearchURL = "https://search.maven.org" - ecosystem = "maven" + DefaultURL = "https://repo1.maven.org/maven2" + SearchURL = "https://search.maven.org" + ecosystem = "maven" maxParentDepth = 5 + // minCoordParts is the minimum number of parts in a Maven coordinate (group:artifact) + minCoordParts = 2 + // coordPartsWithVersion is the number of parts when version is included (group:artifact:version) + coordPartsWithVersion = 3 ) func init() { @@ -50,7 +54,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -132,10 +136,10 @@ type pomDeveloper struct { func ParseCoordinates(coord string) (groupID, artifactID, version string) { // Try colon separator first (traditional maven format) parts := strings.Split(coord, ":") - if len(parts) >= 2 { + if len(parts) >= minCoordParts { groupID = parts[0] artifactID = parts[1] - if len(parts) >= 3 { + if len(parts) >= coordPartsWithVersion { version = parts[2] } return @@ -143,10 +147,10 @@ func ParseCoordinates(coord string) (groupID, artifactID, version string) { // Fall back to slash separator (PURL FullName format) parts = strings.Split(coord, "/") - if len(parts) >= 2 { + if len(parts) >= minCoordParts { groupID = parts[0] artifactID = parts[1] - if len(parts) >= 3 { + if len(parts) >= coordPartsWithVersion { version = parts[2] } } diff --git a/internal/nimble/nimble.go b/internal/nimble/nimble.go index 3ca8192..478ef96 100644 --- a/internal/nimble/nimble.go +++ b/internal/nimble/nimble.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -192,7 +192,7 @@ func (r *Registry) FetchDependencies(ctx context.Context, name, version string) // Examples: "nim >= 1.0", "chronicles", "stew >= 0.1.0" func parseDependency(dep string) (name, requirements string) { dep = strings.TrimSpace(dep) - parts := strings.SplitN(dep, " ", 2) + parts := strings.SplitN(dep, " ", 2) //nolint:mnd // name version split name = parts[0] if len(parts) > 1 { requirements = strings.TrimSpace(parts[1]) diff --git a/internal/npm/npm.go b/internal/npm/npm.go index ccd4f0a..0cedb86 100644 --- a/internal/npm/npm.go +++ b/internal/npm/npm.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -280,7 +280,7 @@ func extractKeywords(v interface{}) []string { func extractNamespace(id string) string { if strings.HasPrefix(id, "@") && strings.Contains(id, "/") { - parts := strings.SplitN(id, "/", 2) + parts := strings.SplitN(id, "/", 2) //nolint:mnd // scope/name split return strings.TrimPrefix(parts[0], "@") } return "" @@ -312,7 +312,7 @@ func (u *URLs) Download(name, version string) string { } shortName := name if strings.Contains(name, "/") { - parts := strings.SplitN(name, "/", 2) + parts := strings.SplitN(name, "/", 2) //nolint:mnd // scope/name split shortName = parts[1] } return fmt.Sprintf("%s/%s/-/%s-%s.tgz", u.baseURL, name, shortName, version) @@ -329,7 +329,7 @@ func (u *URLs) PURL(name, version string) string { namespace := "" pkgName := name if strings.HasPrefix(name, "@") && strings.Contains(name, "/") { - parts := strings.SplitN(name, "/", 2) + parts := strings.SplitN(name, "/", 2) //nolint:mnd // scope/name split namespace = parts[0] pkgName = parts[1] } diff --git a/internal/nuget/nuget.go b/internal/nuget/nuget.go index cef74ad..9639c5c 100644 --- a/internal/nuget/nuget.go +++ b/internal/nuget/nuget.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/packagist/packagist.go b/internal/packagist/packagist.go index 43bc8f9..b3e5bc0 100644 --- a/internal/packagist/packagist.go +++ b/internal/packagist/packagist.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -111,7 +111,7 @@ func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package // Extract namespace (vendor) from name var namespace string - if parts := strings.SplitN(name, "/", 2); len(parts) == 2 { + if parts := strings.SplitN(name, "/", 2); len(parts) == 2 { //nolint:mnd // vendor/package split namespace = parts[0] } diff --git a/internal/pub/pub.go b/internal/pub/pub.go index b013979..e129891 100644 --- a/internal/pub/pub.go +++ b/internal/pub/pub.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/pypi/pypi.go b/internal/pypi/pypi.go index 675b2d3..8ad3782 100644 --- a/internal/pypi/pypi.go +++ b/internal/pypi/pypi.go @@ -45,7 +45,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } @@ -284,7 +284,7 @@ func (r *Registry) FetchDependencies(ctx context.Context, name, version string) func parsePEP508(dep string) (name, requirements, envMarker string) { // Split on ; first to get environment markers - parts := strings.SplitN(dep, ";", 2) + parts := strings.SplitN(dep, ";", 2) //nolint:mnd // dep;marker split nameAndVersion := strings.TrimSpace(parts[0]) if len(parts) > 1 { envMarker = strings.TrimSpace(parts[1]) diff --git a/internal/rubygems/rubygems.go b/internal/rubygems/rubygems.go index f24c18a..5935e85 100644 --- a/internal/rubygems/rubygems.go +++ b/internal/rubygems/rubygems.go @@ -44,7 +44,7 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } diff --git a/internal/terraform/terraform.go b/internal/terraform/terraform.go index ac4b41b..ad7bd57 100644 --- a/internal/terraform/terraform.go +++ b/internal/terraform/terraform.go @@ -44,14 +44,14 @@ func (r *Registry) Ecosystem() string { return ecosystem } -func (r *Registry) URLs() core.URLBuilder { +func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn return r.urls } // parseModuleName parses "namespace/name/provider" format func parseModuleName(name string) (namespace, moduleName, provider string, ok bool) { parts := strings.Split(name, "/") - if len(parts) == 3 { + if len(parts) == 3 { //nolint:mnd // namespace/name/provider return parts[0], parts[1], parts[2], true } return "", "", "", false diff --git a/internal/urlparser/urlparser.go b/internal/urlparser/urlparser.go index 75824c8..e7ec48a 100644 --- a/internal/urlparser/urlparser.go +++ b/internal/urlparser/urlparser.go @@ -116,16 +116,18 @@ func removeChars(s string, chars string) string { // trimGitSuffix removes .git or .git/ suffix case-insensitively func trimGitSuffix(s string) string { - if len(s) >= 4 { - suffix := strings.ToLower(s[len(s)-4:]) - if suffix == ".git" { - return s[:len(s)-4] + const gitSuffix = ".git" + const gitSlashSuffix = ".git/" + if len(s) >= len(gitSuffix) { + suffix := strings.ToLower(s[len(s)-len(gitSuffix):]) + if suffix == gitSuffix { + return s[:len(s)-len(gitSuffix)] } } - if len(s) >= 5 { - suffix := strings.ToLower(s[len(s)-5:]) - if suffix == ".git/" { - return s[:len(s)-5] + if len(s) >= len(gitSlashSuffix) { + suffix := strings.ToLower(s[len(s)-len(gitSlashSuffix):]) + if suffix == gitSlashSuffix { + return s[:len(s)-len(gitSlashSuffix)] } } return s @@ -138,12 +140,13 @@ func removeSchemes(s string) string { // Remove scm:git:, scm:svn:, scm:hg: prefixes sLower := strings.ToLower(s) - if strings.HasPrefix(sLower, "scm:git:") { - s = s[8:] - } else if strings.HasPrefix(sLower, "scm:svn:") { - s = s[8:] - } else if strings.HasPrefix(sLower, "scm:hg:") { - s = s[7:] + switch { + case strings.HasPrefix(sLower, "scm:git:"): + s = s[len("scm:git:"):] + case strings.HasPrefix(sLower, "scm:svn:"): + s = s[len("scm:svn:"):] + case strings.HasPrefix(sLower, "scm:hg:"): + s = s[len("scm:hg:"):] } // Remove standard schemes @@ -192,8 +195,9 @@ func removeScheme(s string) string { func removeAuth(s string) string { // Find @ but make sure we're past any scheme schemeEnd := 0 - if idx := strings.Index(s, "://"); idx != -1 { - schemeEnd = idx + 3 + const schemeSep = "://" + if idx := strings.Index(s, schemeSep); idx != -1 { + schemeEnd = idx + len(schemeSep) } rest := s[schemeEnd:] @@ -278,7 +282,7 @@ func ExtractPath(rawURL string) string { } // Handle user.github.io/repo pattern - if match := githubioRe.FindStringSubmatch(s); len(match) >= 2 { + if match := githubioRe.FindStringSubmatch(s); len(match) >= 2 { //nolint:mnd // regex groups user := match[1] rest := githubioRe.ReplaceAllString(s, "") if rest != "" { @@ -347,7 +351,7 @@ func ExtractHost(rawURL string) string { } // Handle user.github.io pattern - if match := githubioRe.FindStringSubmatch(s); len(match) >= 3 { + if match := githubioRe.FindStringSubmatch(s); len(match) >= 3 { //nolint:mnd // regex groups return "github." + match[2] } diff --git a/registries.go b/registries.go index ebb064a..8a0ff6d 100644 --- a/registries.go +++ b/registries.go @@ -107,7 +107,7 @@ type ( // If client is nil, DefaultClient() is used. // // Supported ecosystems: "cargo", "npm", "gem", "pypi", "golang" -func New(ecosystem string, baseURL string, c *Client) (Registry, error) { +func New(ecosystem string, baseURL string, c *Client) (Registry, error) { //nolint:ireturn return core.New(ecosystem, baseURL, c) } @@ -161,7 +161,7 @@ func ParsePURL(purlStr string) (*PURL, error) { // NewFromPURL creates a registry client from a PURL and returns the parsed components. // Returns the registry, full package name, and version (empty if not in PURL). -func NewFromPURL(purl string, c *Client) (Registry, string, string, error) { +func NewFromPURL(purl string, c *Client) (Registry, string, string, error) { //nolint:ireturn return core.NewFromPURL(purl, c) }