lintfix

package
v0.74.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 3 Imported by: 0

README

lintfix

Structured lint remediation database for Go projects using golangci-lint.

Overview

The lintfix package provides:

  • 📋 Remediation database - Embedded JSON database mapping lint rules to fixes
  • 🔧 Helper references - Links to mogo helper functions for code fixes
  • 📝 Nolint generators - Properly formatted nolint comments with documented reasons
  • 📚 Documentation - Version-specific caveats and best practices

Quick Start

import (
    "github.com/grokify/mogo/lintfix"
    "github.com/grokify/mogo/lintfix/gosec"
)

// Query the remediation database
db := lintfix.MustLoadRemediations()
fix := db.GetGosec("G120")
fmt.Println(fix.Remediation.Summary)
// "Use http.MaxBytesReader inline before parsing form data"

// Generate nolint comments
comment := gosec.NolintG117(gosec.CommonReasons.OAuthTokenResponse)
// "//nolint:gosec // G117: OAuth token response per RFC 6749"
import "github.com/grokify/mogo/lintfix/dupl"

comment := dupl.Nolint(dupl.CommonReasons.ParallelResourceWrapper)
// "//nolint:dupl // Structurally parallel to sibling wrapper methods over
// distinct generated types; not meaningfully extractable without reflection
// or per-type adapters"
import "github.com/grokify/mogo/lintfix/unparam"

comment := unparam.Nolint(unparam.CommonReasons.InterfaceSignature)
// "//nolint:unparam // Signature fixed by an interface method set this type implements"

Remediation Types

Type Description Example
code Add/modify code with helper functions G120: Use http.MaxBytesReader
nolint Add nolint annotation with reason G117: OAuth token response
refactor Broader code changes needed G101: Move secrets to env vars

Supported Linters

  • gosec - Security-focused rules (G101, G112, G115, G117, G118, G120, G122, G124, G401, G404, G501, G601, G703, G704, G705, G706, G710)
  • staticcheck - Static analysis (SA1019, SA4006, QF1003, QF1012)
  • errcheck - Error handling
  • govet - Inline remediation notes
  • dupl - Duplicate code detection; see the dupl subpackage for nolint generators covering the generated-client-wrapper case
  • unparam - Unused function parameters/results; see the unparam subpackage for nolint generators covering interface/callback-constrained signatures
  • unused - Dead code (unused functions, vars, consts, types); the fix is deletion, including any import the removed code solely required

G404: Weak Random Number Generator

G404 flags any use of math/rand or math/rand/v2 — it has no way to tell whether the value is used for something security-sensitive (tokens, keys, nonces, passwords) or not (shuffling display data, jitter, sampling, non-cryptographic test fixtures). Golangci-lint version skew commonly surfaces this: an older locally-installed gosec may not flag a call that a newer one (e.g. CI's version: latest) does, since gosec's G404 detection coverage (which stdlib functions it recognizes, e.g. rand.Shuffle) has expanded across releases.

If the value IS security-sensitive - switch to crypto/rand, don't nolint:

import "crypto/rand"

n, err := rand.Int(rand.Reader, max)
if err != nil {
    return err
}

If the value is NOT security-sensitive - nolint with a reason:

import "math/rand"

//nolint:gosec // G404: Shuffling display data, not security-sensitive
rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] })

G703: Path Traversal

G703 warns about file paths constructed from user input. The fix depends on where your code lives:

In cmd/ (CLI entry points) - User explicitly provides the path, use nolint:

// User provides path via CLI flag - they own the risk
cleanPath := filepath.Clean(userPath)
if err := os.WriteFile(cleanPath, data, 0600); err != nil { //nolint:gosec // G703: Path from CLI flag
    return err
}

In library code - Use secure functions that reject .. sequences:

import "github.com/grokify/mogo/os/osutil"

// Library code - reject paths with traversal sequences
data, err := osutil.ReadFileSecure(path)
if err != nil {
    // Returns: "path contains '..' traversal sequence: ../etc/passwd"
    return err
}

if err := osutil.WriteFileSecure(path, data, 0600); err != nil {
    return err
}

Error returned: osutil.ErrPathTraversal is returned when a path contains ..:

// errors.Is check
if errors.Is(err, osutil.ErrPathTraversal) {
    log.Println("Invalid path:", err)
}

G101: Config Struct Fields Set From Parameters

G101 also fires on struct literals with credential-named fields (ClientSecret, APIKey, Password, Token, ...) even when the values come from caller-supplied parameters, not literals - a common shape for any OAuth/API-client config constructor:

func (s *OAuthService) ConfigureGoogle(clientID, clientSecret, redirectURL string) {
	s.RegisterProvider(&OAuthProvider{ //nolint:gosec // G101: ClientID/ClientSecret are set from caller-supplied parameters, not hardcoded literals
		Name:         "google",
		ClientID:     clientID,
		ClientSecret: clientSecret,
		RedirectURL:  redirectURL,
	})
}

There is no code fix here - the struct shape is the point, and gosec cannot see that the values are parameters rather than literals. nolint is the correct remediation.

G115: Integer Overflow Conversion — Length-Prefixed External Data

G115's default remediation (a nolint backed by "domain knowledge guarantees the value fits") is right for small constants/enums, but wrong for a length/size prefix read off a file format or wire protocol — there the value comes from outside the program and nothing guarantees it fits until you check.

A common bad shape: converting through a narrower signed type first, then validating the signed result:

n := int(int32(binary.LittleEndian.Uint32(lenBuf[:])))
if n < 5 {
    return fmt.Errorf("invalid length %d", n)
}

This only catches lengths that wrapped negative (raw values ≥ 2^31). Any raw value below that — up to 2^31-1, over two billion — sails through as a large positive n and drives make([]byte, n) with an attacker-or-corruption-controlled size, unless something downstream happens to bound it separately.

Verified fix — validate the raw unsigned value's range before converting:

const maxDocSize = 16 * 1024 * 1024 // real domain ceiling, not an arbitrary guess

raw := binary.LittleEndian.Uint32(lenBuf[:])
if raw < 5 || raw > maxDocSize {
    return fmt.Errorf("invalid length %d", raw)
}
n := int(raw) // safe: raw is now proven in [5, maxDocSize]

This clears the G115 finding (the conversion now only ever sees a pre-validated range) and is a strictly stronger real fix than the nolint default: it also closes the unbounded-allocation gap the "check after converting" shape left open. Pick maxDocSize from a real domain limit (a format spec's own max, a protocol's own frame cap) — never an arbitrary round number.

G706: Log Injection

G706 warns when a value derived from client input (request Host, headers, path, etc.) is written directly to a log call, since an unescaped newline or control character lets an attacker forge fake log lines (CWE-117).

Verified fix - wrap with strconv.Quote, not just the %q verb:

import "strconv"

// Correct: strconv.Quote is a recognized sanitizer, clears the finding
log.Printf("Proxy error for %s: %v", strconv.Quote(r.Host), err)
// Does NOT clear the finding: gosec inspects the argument expression, not the
// format verb, so the raw tainted value is still flagged even with %q
log.Printf("Proxy error for %q: %v", r.Host, err) // still G706

Prefer this code fix over nolint in library code - it's a real fix (escapes injected control characters), not just linter appeasement, and it's what gosec.NolintG706 is documented to defer to.

G101: Environment Variable Names and Enum/Const Identifiers

G101's identifier-name heuristic also fires on constants whose Go name merely contains a credential-flagged substring (secret, cred, apikey, ...) even though the value is not a secret at all - an environment variable name to read at runtime, or a plain enum tag:

const (
	EnvAPIKey = "POSTMAN_API_KEY" //nolint:gosec // G101: This is an environment variable name, not a credential
)

const (
	SecretTypeOriginTeamRegex SecretTypeOrigin = "TEAM_REGEX" //nolint:gosec // G101: Enum/constant identifier matches the credential-name heuristic, but the value is a public tag, not a secret
)

Use gosec.CommonReasons.EnvVarName and gosec.CommonReasons.EnumTagNotCredential for these. gosec's match is per-identifier, not per-const block, so only annotate the specific line(s) it actually flags - a sibling constant in the same block often isn't flagged at all.

G710: Open Redirect

G710 warns when an http.Redirect target is built by concatenating request-derived data (e.g. "https://" + r.Host + r.RequestURI), since an attacker who controls the Host header could make the server redirect anywhere (CWE-601).

Verified fix - build the target with net/url.URL, not string concatenation:

import "net/url"

// Correct: url.URL{}.String() is the recognized safe code shape, clears the finding
target := url.URL{Scheme: "https", Host: r.Host, Path: r.URL.Path, RawQuery: r.URL.RawQuery}
http.Redirect(w, r, target.String(), http.StatusMovedPermanently)

Important - this clears the linter, not the actual vulnerability. Verified empirically: gosec accepts the url.URL{} construction on its own, with no host validation at all. The real security fix is a separate step - validate the host against a known allowlist (e.g. the backends your proxy actually serves) before redirecting:

if !isKnownHost(r.Host) { // e.g. rp.findProxy(r.Host) != nil in a reverse proxy
    http.NotFound(w, r)
    return
}
target := url.URL{Scheme: "https", Host: r.Host, Path: r.URL.Path, RawQuery: r.URL.RawQuery}
http.Redirect(w, r, target.String(), http.StatusMovedPermanently)

Do both. Do not treat "gosec is clean" as evidence that a request-derived redirect target is actually safe.

dupl: Structurally Parallel Generated-Client Wrappers

dupl's default remediation - extract a shared helper - is usually right. But one shape recurs across generated-client wrappers (ogen, openapi-generator, protoc): sibling methods per resource kind (CreateFolder / CreateRequest / CreateResponse, or GetX / DeleteX repeated per X) that each switch over a distinct, codegen-produced response/error union:

func (s *Service) GetFolder(ctx context.Context, collectionID, folderID string, opts *GetOptions) (*FolderResult, error) {
	// ...
	switch r := res.(type) {
	case *api.CollectionFolderInfo:
		// ...
	case *api.GetCollectionFolderNotFound:
		return nil, postmanerr.FromProblemDetails([]byte(*r), http.StatusNotFound)
	// ...
	}
}

//nolint:dupl // Structurally parallel to sibling wrapper methods over distinct generated types; not meaningfully extractable without reflection or per-type adapters
func (s *Service) GetRequest(ctx context.Context, collectionID, requestID string, opts *GetOptions) (*RequestResult, error) {
	// ...
	switch r := res.(type) {
	case *api.CollectionRequestInfo:  // <- unrelated type to CollectionFolderInfo
		// ...
	case *api.GetCollectionRequestNotFound:  // <- unrelated type to GetCollectionFolderNotFound
		return nil, postmanerr.FromProblemDetails([]byte(*r), http.StatusNotFound)
	// ...
	}
}

CollectionFolderInfo and CollectionRequestInfo share no common interface - a real extraction needs reflection or a per-type adapter layer, which is harder to follow than the duplication it removes. Use dupl.Nolint from the dupl subpackage:

import "github.com/grokify/mogo/lintfix/dupl"

comment := dupl.Nolint(dupl.CommonReasons.ParallelResourceWrapper)
// "//nolint:dupl // Structurally parallel to sibling wrapper methods over
// distinct generated types; not meaningfully extractable without reflection
// or per-type adapters"

The same reasoning applies to test files: standalone, one-test-per-endpoint httptest cases are usually clearer than a table-driven consolidation forced just to satisfy dupl. Use dupl.CommonReasons.StandaloneTestClarity there.

Reach for the real refactor first when the duplicated blocks operate on the same concrete type, or the difference is a single value trivial to lift into a function parameter - see remediations.json's dupl.duplicate entry for the general case.

unparam: Unused Parameters and Results

unparam (mvdan.cc/unparam) flags a function parameter (or result) that never actually varies across its call sites - most often leftover generality from an earlier version of the function, and especially common in test helpers as call sites accumulate over time:

// unparam: category always receives ClaimStatistical
func verifiedClaim(id string, category ClaimCategory) Claim {
    return Claim{ID: id, Category: category}
}

The default remediation - delete the parameter, hardcode the constant - is almost always right in unexported code, and is a real simplification, not just linter appeasement:

func verifiedClaim(id string) Claim {
    return Claim{ID: id, Category: ClaimStatistical}
}

Exception: the signature is constrained by something other than its own call sites. unparam only sees call sites within the analyzed code - it can't see that a signature is fixed by an interface method set, a function-type variable (http.HandlerFunc, sort.Interface, a callback struct field), or an exported API whose signature is a compatibility contract. Deleting the parameter there isn't possible (or isn't safe) without breaking the thing the signature exists to satisfy - use nolint:

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { //nolint:unparam // r required by http.Handler
    w.WriteHeader(http.StatusOK)
}

Use the unparam subpackage to generate the comment:

import "github.com/grokify/mogo/lintfix/unparam"

comment := unparam.Nolint(unparam.CommonReasons.InterfaceSignature)
// "//nolint:unparam // Signature fixed by an interface method set this type implements"

Nolint Generators

The gosec subpackage provides type-safe nolint comment generators:

gosec.NolintG101(reason)  // Hardcoded credentials (false positive)
gosec.NolintG115(reason)  // Integer overflow (bounded value)
gosec.NolintG117(reason)  // Secret in JSON response
gosec.NolintG118(reason)  // context.Background in goroutine
gosec.NolintG122(reason)  // filepath.Walk TOCTOU race (cmd/ entry point only)
gosec.NolintG404(reason)  // Weak random number generator (non-security use only; use crypto/rand otherwise)
gosec.NolintG124(reason)  // Insecure cookie attributes (set dynamically/from config)
gosec.NolintG703(reason)  // Path traversal (CLI entry point only)
gosec.NolintG704(reason)  // SSRF (trusted URL)
gosec.NolintG705(reason)  // XSS (trusted content)
gosec.NolintG706(reason)  // Log injection (prefer the strconv.Quote code fix instead)
gosec.NolintG710(reason)  // Open redirect (prefer the url.URL{} code fix instead)

The dupl subpackage provides the equivalent for duplicate-code findings:

dupl.Nolint(reason)  // Structurally-required duplication (see "dupl" section above)

The unparam subpackage provides the equivalent for unused parameter/result findings:

unparam.Nolint(reason)  // Signature constrained by an interface, callback, or exported API (see "unparam" section above)
Common Reasons

Pre-written reason strings for common scenarios:

gosec.CommonReasons.OAuthTokenResponse        // G117
gosec.CommonReasons.ShutdownHandler           // G118
gosec.CommonReasons.PathFromCLIFlag           // G703
gosec.CommonReasons.HttptestServer            // G704
gosec.CommonReasons.BoundedByValidation       // G115
gosec.CommonReasons.ParameterNotLiteral       // G101 - config struct field set from a parameter
gosec.CommonReasons.EnvVarName                // G101 - environment variable name, not a credential
gosec.CommonReasons.EnumTagNotCredential       // G101 - enum/const identifier matches heuristic, value is a public tag
gosec.CommonReasons.TestControlledInputNoUntrustedSource // G706 - nolint fallback only; prefer strconv.Quote
gosec.CommonReasons.ShufflingDisplayData       // G404 - non-security use only; use crypto/rand otherwise

dupl.CommonReasons.ParallelResourceWrapper    // sibling wrapper methods over distinct generated types
dupl.CommonReasons.StandaloneTestClarity      // standalone per-endpoint test, not worth consolidating

unparam.CommonReasons.InterfaceSignature      // parameter required by an interface method set
unparam.CommonReasons.CallbackSignature       // parameter required by a callback/function-type value
unparam.CommonReasons.ExportedAPICompat       // parameter kept for exported API compatibility

Documentation

Adding New Rules

Edit remediations.json to add new rules:

{
  "linters": {
    "gosec": {
      "G999": {
        "name": "Rule name",
        "description": "What the rule detects",
        "severity": "high|medium|low",
        "category": "security|correctness|maintenance",
        "remediation": {
          "type": "code|nolint|refactor",
          "summary": "Brief fix description",
          "example": "Code example"
        }
      }
    }
  }
}

Documentation

Overview

Package lintfix provides a structured database of lint rule remediations for Go projects using golangci-lint.

This package serves as a "data overlay" that maps lint errors to:

  • Remediation strategies (code fix, nolint annotation, refactor)
  • Helper packages that provide actual fixes (within mogo)
  • Pre-written nolint comments with proper documentation
  • Example code and explanations

Usage

Load the remediation database and query for specific rules:

db := lintfix.MustLoadRemediations()
fix := db.GetGosec("G120")
fmt.Println(fix.Remediation.Summary)
// "Use http.MaxBytesReader before parsing form data"

Remediation Types

The database categorizes remediations into three types:

  • "code": Fix by adding/changing code (e.g., LimitRequestBody for G120)
  • "nolint": Fix by adding a nolint annotation with proper documentation
  • "refactor": Fix requires broader code changes (e.g., removing hardcoded secrets)

Nolint Generators

For rules that require nolint annotations, use the gosec subpackage:

comment := gosec.NolintG117(gosec.CommonReasons.OAuthTokenResponse)
// Returns: "//nolint:gosec // G117: OAuth token response per RFC 6749"

Helper Package References

Code-based remediations reference helper packages within mogo:

fix := db.GetGosec("G120")
fmt.Println(fix.Remediation.Package)
// "github.com/grokify/mogo/net/http/httputilmore"
fmt.Println(fix.Remediation.Function)
// "LimitRequestBody"

Supported Linters

Currently supported:

  • gosec: Security-focused linter
  • staticcheck: Go static analysis
  • errcheck: Error handling checks
  • govet: Go vet diagnostics
  • dupl: Duplicate code detection
  • unparam: Unused function parameters/results

Documentation

For detailed guides including version-specific caveats, see: https://github.com/grokify/mogo/tree/main/docs/lintfix

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Remediation

type Remediation struct {
	Type        string   `json:"type"` // "code", "nolint", "refactor"
	Summary     string   `json:"summary"`
	Pattern     string   `json:"pattern,omitempty"`
	Package     string   `json:"package,omitempty"`
	Function    string   `json:"function,omitempty"`
	Example     string   `json:"example,omitempty"`
	Explanation string   `json:"explanation,omitempty"`
	When        string   `json:"when,omitempty"`
	Avoid       []string `json:"avoid,omitempty"`
	Caveats     []string `json:"caveats,omitempty"`
}

Remediation contains the actual fix information.

type RemediationDB

type RemediationDB struct {
	Version     string                         `json:"version"`
	Description string                         `json:"description"`
	Linters     map[string]map[string]*RuleFix `json:"linters"`
}

RemediationDB is the top-level structure for the remediation database.

func LoadRemediations

func LoadRemediations() (*RemediationDB, error)

LoadRemediations loads and parses the embedded remediation database.

func MustLoadRemediations

func MustLoadRemediations() *RemediationDB

MustLoadRemediations loads the remediation database or panics.

func (*RemediationDB) Get

func (db *RemediationDB) Get(linter, code string) *RuleFix

Get retrieves a remediation by linter and rule code. Returns nil if not found.

func (*RemediationDB) GetGosec

func (db *RemediationDB) GetGosec(code string) *RuleFix

GetGosec is a convenience method for getting gosec remediations.

func (*RemediationDB) GetStaticcheck

func (db *RemediationDB) GetStaticcheck(code string) *RuleFix

GetStaticcheck is a convenience method for getting staticcheck remediations.

func (*RemediationDB) ListLinters

func (db *RemediationDB) ListLinters() []string

ListLinters returns all linters in the database.

func (*RemediationDB) ListRules

func (db *RemediationDB) ListRules(linter string) []string

ListRules returns all rule codes for a given linter.

type RuleFix

type RuleFix struct {
	Name        string       `json:"name"`
	Description string       `json:"description"`
	Severity    string       `json:"severity,omitempty"`
	Category    string       `json:"category,omitempty"`
	Remediation *Remediation `json:"remediation"`
	References  []string     `json:"references,omitempty"`
}

RuleFix contains remediation information for a specific lint rule.

func (*RuleFix) HasHelper

func (rf *RuleFix) HasHelper() bool

HasHelper returns true if this remediation has a helper function.

func (*RuleFix) String

func (rf *RuleFix) String() string

String returns a formatted description of the rule fix.

Directories

Path Synopsis
Package gosec provides helpers for generating nolint comments for gosec rules.
Package gosec provides helpers for generating nolint comments for gosec rules.
Package unparam provides helpers for generating nolint comments for the unparam linter (mvdan.cc/unparam), which flags a function parameter (or result) that never varies across its call sites.
Package unparam provides helpers for generating nolint comments for the unparam linter (mvdan.cc/unparam), which flags a function parameter (or result) that never varies across its call sites.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL