Documentation
¶
Overview ¶
Package humanizelint detects hand-rolled reimplementations of github.com/dustin/go-humanize and suggests the library function instead.
The linter scans Go source files using AST pattern matching. Each rule looks for a cluster of signals that, together, strongly indicate a manual reimplementation of a specific go-humanize feature. Single weak signals are never enough — the rules require multiple corroborating signals within the same function to keep false positives near zero.
Rules ¶
H001 manual-bytes-format → humanize.Bytes / humanize.IBytes H002 manual-comma-format → humanize.Comma / humanize.Commaf H003 manual-reltime-format → humanize.RelTime / humanize.Time H004 manual-plural → humanize.Plural / humanize.PluralWord H005 manual-si-format → humanize.SI / humanize.SIWithDigits H006 manual-ftoa → humanize.Ftoa / humanize.FtoaWithDigits H007 manual-parse-bytes → humanize.ParseBytes
Usage as a library:
import (
humanizelint "github.com/larsartmann/go-humanize-linter"
"github.com/larsartmann/go-linter-sdk"
)
reg := humanizelint.DefaultRegistry()
report, err := reg.Run(ctx, ".")
fmt.Println(linter.ExitCodeFromReport(report))
Index ¶
- Constants
- func AllRules() []linter.RuleFunc
- func DefaultRegistry() *linter.Registry
- func DetectFuncDecl(fset *token.FileSet, file *ast.File, fn *ast.FuncDecl, filePath string) []finding.Finding
- func IsGeneratedFile(path string, content []byte) bool
- func RuleBytes() linter.RuleFunc
- func RuleComma() linter.RuleFunc
- func RuleCommaf() linter.RuleFunc
- func RuleFtoa() linter.RuleFunc
- func RuleOrdinal() linter.RuleFunc
- func RuleParseBytes() linter.RuleFunc
- func RulePlural() linter.RuleFunc
- func RuleRelTime() linter.RuleFunc
- func RuleSI() linter.RuleFunc
- func VerifySuppressionComment(comment string) (bool, bool)
- func VerifySuppressions(dir string, report *finding.Report) ([]finding.Finding, error)
- func VerifySuppressionsInFiles(fset *token.FileSet, files []*ast.File, report *finding.Report) []finding.Finding
- type HumanizeDetector
- type ParsedFile
- type SuppressionDirective
- type WalkError
Examples ¶
Constants ¶
const ( RuleIDH001 = "H001" RuleIDH002 = "H002" RuleIDH003 = "H003" RuleIDH004 = "H004" RuleIDH005 = "H005" RuleIDH006 = "H006" RuleIDH007 = "H007" RuleIDH008 = "H008" RuleIDH009 = "H009" )
Rule ID constants — the single source of truth for rule identifiers. All packages (CLI, plugin, detectors) must reference these instead of string literals to prevent drift when rules are added or renamed.
const RuleIDH0SUP = "H0SUP"
RuleIDH0SUP is the pseudo-rule ID for suppression-verification diagnostics. It is NOT in AllRules() and cannot be enabled/disabled via --enable/--disable. It is produced only by VerifySuppressions and must bypass confidence filtering so that stale or misspelled //nolint directives are always surfaced.
Variables ¶
This section is empty.
Functions ¶
func AllRules ¶
AllRules returns every rule in this linter as a slice. Useful for consumers that want to cherry-pick rules into their own registry.
Example ¶
ExampleAllRules lists the stable rule IDs in registration order.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
for _, rule := range humanizelint.AllRules() {
fmt.Println(rule.Meta.ID)
}
}
Output: H001 H002 H003 H004 H005 H006 H007 H008 H009
func DefaultRegistry ¶
DefaultRegistry returns a Registry pre-loaded with all humanize-lint rules, all enabled by default.
Example ¶
ExampleDefaultRegistry shows building a registry with every rule enabled by default — the typical starting point for running the full linter.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
registry := humanizelint.DefaultRegistry()
fmt.Println("registered rules:", len(registry.All()))
}
Output: registered rules: 9
func DetectFuncDecl ¶
func DetectFuncDecl(fset *token.FileSet, file *ast.File, fn *ast.FuncDecl, filePath string) []finding.Finding
DetectFuncDecl runs all enabled rules against a single *ast.FuncDecl and returns any findings. This is the per-function entry point used by the golangci-lint plugin wrapper (plugin/plugin.go) which iterates over pass.Files instead of walking a directory.
Suppression via //nolint:gohumanize (or //nolint:all, or scoped //nolint:gohumanize:H001) is honoured per-rule. A bare //nolint suppresses every rule; //nolint:gohumanize suppresses only this linter.
Example ¶
ExampleDetectFuncDecl runs the detectors against a hand-rolled byte formatter parsed from source and reports how many findings the KMGTPE trick produces.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
const src = `package main
import "fmt"
func formatBytes(b uint64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := uint64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
}
`
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "demo.go", src, parser.ParseComments)
if err != nil {
panic(err)
}
findings := 0
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
findings += len(humanizelint.DetectFuncDecl(fset, file, fn, "demo.go"))
}
fmt.Println("findings:", findings)
}
Output: findings: 1
func IsGeneratedFile ¶ added in v0.2.0
IsGeneratedFile reports whether a file looks like auto-generated Go code. The check covers every generator supported by github.com/LarsArtmann/gogenfilter/v3 (sqlc, templ, protobuf, deepcopy-gen, wire, moq, mockgen, mockery, easyjson, counterfeiter, etc.) plus the generic "// Code generated by" fallback per https://go.dev/s/generatedcode.
Two-phase detection is delegated to gogenfilter:
- Filename-only phase (zero I/O) — runs whenever path is non-empty.
- Content phase — runs only when content is non-empty. Pass nil to skip the content phase (e.g. from the golangci-lint plugin runtime, where the file content is already held by the analyzer and reading again would be wasteful).
Callers that already read the file (the walker) should pass the content; callers that have only the path (the plugin) should pass nil.
Compatibility note: gogenfilter does not enumerate every tooling convention. In particular, the generic `_gen.go` and `.gen.go` suffixes (used by go-enum-output, deep-copy generators, hand-rolled `go generate` outputs, etc.) are NOT detected by gogenfilter's table (it only knows specific patterns like `wire_gen.go`). To preserve the original behaviour of the inline checks this helper replaced, we run a small fallback that catches those three conventions when gogenfilter reports nothing.
func RuleBytes ¶
RuleBytes (H001) detects manual byte-size formatting that should use humanize.Bytes or humanize.IBytes.
Triggers when a function contains any of these signal clusters:
- "KMGTPE"[exp] index expression (the classic prefix trick)
- A []string literal with 2+ byte-unit strings (KB, MB, KiB, MiB, etc.)
- 2+ distinct byte-unit strings in format strings + division by a power of 1024
- 3+ distinct byte-unit strings (very strong signal even without explicit division)
Example ¶
ExampleRuleBytes inspects a single rule factory's metadata.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RuleBytes()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H001 manual-bytes-format
func RuleComma ¶
RuleComma (H002) detects manual comma (thousands separator) insertion that should use humanize.Comma or humanize.Commaf.
Triggers via two detection paths:
- Strong: modulo-3 or step-by-3 grouping + comma writing (high/full confidence)
- Fallback: for-loop + comma writing + digit conversion (medium confidence) — catches cases where the step size is a named constant like digitsPerGroup
Example ¶
ExampleRuleComma shows the H002 manual-comma-format rule.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RuleComma()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H002 manual-comma-format
func RuleCommaf ¶ added in v0.2.0
RuleCommaf (H009) detects manual float-with-thousands-separator formatting that should use humanize.Commaf.
Triggers when a function contains BOTH a "%.Nf" fmt.Sprintf call AND manual comma/separator writing. The combination is a near-certain reimplementation of humanize.Commaf.
Example ¶
ExampleRuleCommaf shows the H009 manual-commaf rule.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RuleCommaf()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H009 manual-commaf
func RuleFtoa ¶
RuleFtoa (H006) detects manual float-to-string conversion with trailing-zero stripping that should use humanize.Ftoa.
Triggers when a function contains:
strings.TrimRight(strings.TrimRight(x, "0"), ".")
This nested-TrimRight pattern is the textbook reimplementation of humanize.Ftoa's trailing-zero removal.
Example ¶
ExampleRuleFtoa shows the H006 manual-ftoa rule.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RuleFtoa()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H006 manual-ftoa
func RuleOrdinal ¶ added in v0.2.0
RuleOrdinal (H008) detects hand-rolled English ordinal-suffix formatting (1st, 2nd, 3rd, 4th) that should use humanize.Ordinal.
Triggers when a function contains a switch on n%10 or n%100 whose case branches return at least 3 of the 4 ordinal suffixes ("st", "nd", "rd", "th"). Three distinct suffixes is enough to flag — the fourth is almost always the "default" case.
Example ¶
ExampleRuleOrdinal shows the H008 manual-ordinal rule.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RuleOrdinal()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H008 manual-ordinal
func RuleParseBytes ¶
RuleParseBytes (H007) detects manual byte-size string parsing ("10MB" → bytes) that should use humanize.ParseBytes.
Triggers when a function contains:
- 2+ strings.HasSuffix/CutSuffix/TrimSuffix calls checking byte-unit suffixes
- OR a map[string]int64 literal with 2+ byte-unit keys used as multipliers
Also detects package-level var declarations with byte-unit multiplier maps.
Example ¶
ExampleRuleParseBytes shows the H007 manual-parse-bytes rule.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RuleParseBytes()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H007 manual-parse-bytes
func RulePlural ¶
RulePlural (H004) detects manual English pluralization that should use github.com/dustin/go-humanize/english.Plural or english.PluralWord.
Triggers when a function:
- Has parameters named "singular" and "plural" (explicit reimplementation)
- OR contains an `if x == 1` / `if x != 1` conditional where the branches return different string values (the classic plural switch)
Example ¶
ExampleRulePlural shows the H004 manual-plural rule.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RulePlural()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H004 manual-plural
func RuleRelTime ¶
RuleRelTime (H003) detects manual relative-time formatting ("3 hours ago", "in 2 days") that should use humanize.RelTime or humanize.Time.
Triggers when a function contains BOTH:
- A time-difference computation (time.Since or .Sub call)
- A string literal containing "ago", "from now", or "just now"
AND at least one time-threshold comparison (time.Minute, time.Hour, etc.) to suppress false positives where "ago" appears in unrelated contexts.
Example ¶
ExampleRuleRelTime shows the H003 manual-reltime-format rule.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RuleRelTime()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H003 manual-reltime-format
func RuleSI ¶
RuleSI (H005) detects manual SI-prefix formatting ("1.2K", "3.4M") that should use humanize.SI or humanize.SIWithDigits.
Triggers when a function contains BOTH:
- Division by 1000 or 1000000 (or 1000000000)
- A standalone "K" or "M" string literal used as a suffix
This is distinct from H001 (byte formatting) because SI uses 1000-base and does not append "B".
Example ¶
ExampleRuleSI shows the H005 manual-si-format rule.
package main
import (
"fmt"
humanizelint "github.com/larsartmann/go-humanize-linter"
)
func main() {
rule := humanizelint.RuleSI()
fmt.Println(rule.Meta.ID, rule.Meta.Name)
}
Output: H005 manual-si-format
func VerifySuppressionComment ¶ added in v0.2.0
VerifySuppressionComment is a convenience helper for tests: it parses a single comment string and returns whether it suppresses our linter and whether it contains a misspelled linter name.
func VerifySuppressions ¶ added in v0.2.0
VerifySuppressions reports two classes of suppression problems:
- Unknown linter names that look like "gohumanize" (e.g. "go-humanize-linter").
- //nolint:gohumanize[:Hxxx] directives that did not suppress any finding.
The returned findings use the pseudo-rule ID "H0SUP" so they are clearly verification diagnostics, not humanize reimplementation findings.
func VerifySuppressionsInFiles ¶ added in v0.2.0
func VerifySuppressionsInFiles( fset *token.FileSet, files []*ast.File, report *finding.Report, ) []finding.Finding
VerifySuppressionsInFiles is the plugin-path variant of VerifySuppressions. It collects directives from pre-parsed files (pass.Files) instead of walking a directory, making it suitable for use in analysis.Analyzer.Run where the files are already parsed by the driver.
Types ¶
type HumanizeDetector ¶ added in v0.2.0
type HumanizeDetector struct {
// contains filtered or unexported fields
}
HumanizeDetector is the facade that owns the per-function detection pipeline: it iterates over every detector, applies per-rule suppression, and returns the aggregated findings. Use it instead of DetectFuncDecl when you need to configure the detector set, plug a custom suppression resolver, or share state (e.g. metrics) across calls.
Construction:
detector := humanizelint.NewHumanizeDetector() // all 9 rules enabled
detector := humanizelint.NewHumanizeDetector( // opt-in subset
humanizelint.RuleBytes(),
humanizelint.RuleComma(),
)
Then run it on a single function:
findings := detector.Run(fset, file, fn, "demo.go")
func NewHumanizeDetector ¶ added in v0.2.0
func NewHumanizeDetector(rules ...linter.RuleFunc) *HumanizeDetector
NewHumanizeDetector constructs a HumanizeDetector running the given rules in the given order. If no rules are passed, all 9 default rules are registered.
func (*HumanizeDetector) Run ¶ added in v0.2.0
func (d *HumanizeDetector) Run( fset *token.FileSet, file *ast.File, fn *ast.FuncDecl, filePath string, ) []finding.Finding
Run applies every registered detector to a single function, honouring per-rule //nolint:gohumanize[:Hxxx] suppression directives. Returns the aggregated findings (nil if none).
type ParsedFile ¶
ParsedFile holds a parsed Go source file together with its token file set.
func WalkGoDir ¶
func WalkGoDir(dir string) ([]ParsedFile, error)
WalkGoDir walks dir recursively, parses every non-test .go file, and returns them. Files that fail to parse are silently skipped — syntax errors are the compiler's job, not the linter's. On walk failure it returns a *WalkError wrapping the underlying fs error. Callers should errors.AsType[*WalkError](err) to read .Dir.
Each file is read once and the content is reused for both the generated-file check (via gogenfilter) and the AST parser. This keeps the walker I/O-free beyond a single read per file and gives full two-phase generated detection.
Always returns *WalkError (an error type) — signature stays `error` for v0.1.x API compatibility.
type SuppressionDirective ¶ added in v0.2.0
type SuppressionDirective struct {
FilePath string
FunctionLine int
Line int
Column int
RawText string
Suppressed []string
}
SuppressionDirective records a //nolint-style directive that is attached to a function declaration. It is used by --verify-suppressions to detect stale or misspelled suppressions.
type WalkError ¶ added in v0.2.0
WalkError is returned by WalkGoDir and checkFuncDecls when the directory walk itself fails. It carries the directory the walker was asked to scan so callers can produce actionable error messages without parsing the error string.
Source Files
¶
- doc.go
- pattern_bytes.go
- pattern_comma.go
- pattern_commaf.go
- pattern_ftoa.go
- pattern_generated.go
- pattern_helpers.go
- pattern_ordinal.go
- pattern_parsebytes.go
- pattern_plural.go
- pattern_si.go
- pattern_time.go
- rule_bytes.go
- rule_comma.go
- rule_commaf.go
- rule_ftoa.go
- rule_ordinal.go
- rule_parsebytes.go
- rule_plural.go
- rule_reltime.go
- rule_si.go
- rules.go
- suppression.go
- walker.go
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
go-humanize-linter
command
Command go-humanize-linter scans Go source files for hand-rolled reimplementations of github.com/dustin/go-humanize and reports them as findings.
|
Command go-humanize-linter scans Go source files for hand-rolled reimplementations of github.com/dustin/go-humanize and reports them as findings. |
|
gohumanize
command
Command gohumanize runs the go-humanize-linter as a standalone analysis.Analyzer via singlechecker.Main.
|
Command gohumanize runs the go-humanize-linter as a standalone analysis.Analyzer via singlechecker.Main. |
|
Package plugin exposes go-humanize-linter as a golangci-lint v2 custom linter module plugin.
|
Package plugin exposes go-humanize-linter as a golangci-lint v2 custom linter module plugin. |