-
Notifications
You must be signed in to change notification settings - Fork 313
Feat: Add confusing-epoch rule #1556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Feat: Add confusing-epoch rule #1556
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a new linting rule confusing-epoch to detect confusingly named variables when working with epoch time values. The rule enforces that variables initialized with time methods like Unix(), UnixMilli(), UnixMicro(), and UnixNano() should have names that clearly indicate their time unit (e.g., ending with "Sec", "Ms", "Nano", etc.) to prevent confusion and bugs when working with different time scales.
Key changes:
- New rule implementation that validates variable naming for epoch time assignments
- Comprehensive test data covering various edge cases (mixed case, suffix positioning, different time units)
- Documentation and configuration updates to register the rule
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| rule/confusing_epoch.go | Implements the new ConfusingEpochRule with AST walking logic to check variable names initialized with epoch time methods |
| testdata/confusing_epoch.go | Comprehensive test data with expected MATCH patterns covering valid/invalid naming scenarios |
| test/confusing_epoch_test.go | Test harness following the standard pattern used by other rules |
| config/config.go | Registers the new rule in the allRules list |
| RULES_DESCRIPTIONS.md | Documents the rule with description, examples, and configuration details |
| README.md | Adds the rule to the main rules table |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return "confusing-epoch" | ||
| } | ||
|
|
||
| type confusingEpoch struct { |
Copilot
AI
Nov 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type name confusingEpoch doesn't follow the naming convention used by other rules in this codebase. Other walker types are prefixed with lint (e.g., lintTimeNames, lintTimeEqual, lintConfusingNames). This type should be renamed to lintConfusingEpoch for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not tied to this name but it seemed logical to me.
| &rule.InefficientMapLookupRule{}, | ||
| &rule.ForbiddenCallInWgGoRule{}, | ||
| &rule.UnnecessaryIfRule{}, | ||
| &rule.ConfusingEpochRule{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this rule enforces naming for the vars, I propose to rename it to epoch-naming for consistency with error-naming and var-naming.
| w.onFailure(lint.Failure{ | ||
| Confidence: 0.9, | ||
| Node: name, | ||
| Category: lint.FailureCategoryTime, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Category: lint.FailureCategoryTime, | |
| Category: lint.FailureCategoryNaming, |
| import "time" | ||
|
|
||
| var ( | ||
| creation = time.Now().Unix() // MATCH /variable 'creation' initialized with Unix() should have a name end with one of [Sec Second Seconds]/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we simplify?
| creation = time.Now().Unix() // MATCH /variable 'creation' initialized with Unix() should have a name end with one of [Sec Second Seconds]/ | |
| creation = time.Now().Unix() // MATCH /var creation should have a suffix Sec, Second, Seconds/ |
Similar rules:
revive/testdata/golint/var_naming.go
Lines 15 to 50 in 7a40133
| var safeUrl = "HttPS://..iaMHost..Test:443/paTh^A%ef//./%41PaTH/..//?" // MATCH /var safeUrl should be safeURL/ | |
| var var_name int // MATCH /don't use underscores in Go names; var var_name should be varName/ | |
| type t_wow struct { // MATCH /don't use underscores in Go names; type t_wow should be tWow/ | |
| x_damn int // MATCH /don't use underscores in Go names; struct field x_damn should be xDamn/ | |
| Url *url.URL // MATCH /struct field Url should be URL/ | |
| } | |
| const fooId = "blah" // MATCH /const fooId should be fooID/ | |
| func f_it() { // MATCH /don't use underscores in Go names; func f_it should be fIt/ | |
| more_underscore := 4 // MATCH /don't use underscores in Go names; var more_underscore should be moreUnderscore/ | |
| _ = more_underscore | |
| var err error | |
| if isEof := (err == io.EOF); isEof { // MATCH /var isEof should be isEOF/ | |
| more_underscore = 7 // should be okay | |
| } | |
| x := net_http.Request{} // should be okay | |
| _ = x | |
| var ips []net.IP | |
| for _, theIp := range ips { // MATCH /range var theIp should be theIP/ | |
| _ = theIp | |
| } | |
| switch myJson := g(); { // MATCH /var myJson should be myJSON/ | |
| default: | |
| _ = myJson | |
| } | |
| var y net_http.ResponseWriter // an interface | |
| switch tApi := y.(type) { // MATCH /var tApi should be tAPI/ | |
| default: | |
| _ = tApi | |
| } | |
revive/testdata/golint/error_naming.go
Lines 11 to 37 in 7a40133
| var unexp = errors.New("some unexported error") // MATCH /error var unexp should have name of the form errFoo/ | |
| // Exp ... | |
| var Exp = errors.New("some exported error") | |
| // MATCH:14 /error var Exp should have name of the form ErrFoo/ | |
| var ( | |
| e1 = fmt.Errorf("blah %d", 4) // MATCH /error var e1 should have name of the form errFoo/ | |
| // E2 ... | |
| E2 = fmt.Errorf("blah %d", 5) // MATCH /error var E2 should have name of the form ErrFoo/ | |
| // check there is no false positive for blank identifier. | |
| // This pattern that can be found in benchmarks and examples should be allowed. | |
| // The fact that the error variable is not used is out of the scope of this rule. | |
| _ = errors.New("ok") | |
| ) | |
| func f() { | |
| // the linter does not check local variables, this one is valid | |
| var whatever = errors.New("ok") | |
| _ = whatever | |
| // same as above with a blank identifier | |
| _ = errors.New("ok") | |
| } |
| }, | ||
| } | ||
|
|
||
| file.Pkg.TypeCheck() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type check might fail, do we care?
| for i, name := range v.Names { | ||
| // Skip if no initialization value | ||
| if i >= len(v.Values) { | ||
| continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why continue and not break ? My understanding is that if i >= len(v.Values) is true at iteration i it will be true in subsequent iterations.
| // Handle var declarations | ||
| for i, name := range v.Names { | ||
| // Skip if no initialization value | ||
| if i >= len(v.Values) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not need to evaluate the expression len(v.Values) at each iteration of the loop, we can just evaluate ones before entering the loop
| if i >= len(v.Values) { | ||
| continue | ||
| } | ||
| w.check(name, v.Values[i]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| w.check(name, v.Values[i]) | |
| w.check(name, v.Values[i]) |
| if v.Tok != token.DEFINE && v.Tok != token.ASSIGN { | ||
| return w | ||
| } | ||
| for i, lhs := range v.Lhs { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| for i, lhs := range v.Lhs { | |
| for i, lhs := range v.Lhs { |
| } | ||
| for i, lhs := range v.Lhs { | ||
| if i >= len(v.Rhs) { | ||
| continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
break ?
| return w | ||
| } | ||
| for i, lhs := range v.Lhs { | ||
| if i >= len(v.Rhs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The evaluation of len(v.Rhs) will result in the same value for every iteration of the loop, we can move this expression out of the loop
| if ident, ok := lhs.(*ast.Ident); ok { | ||
| // Skip blank identifier | ||
| if ident.Name == "_" { | ||
| continue | ||
| } | ||
| w.check(ident, v.Rhs[i]) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if ident, ok := lhs.(*ast.Ident); ok { | |
| // Skip blank identifier | |
| if ident.Name == "_" { | |
| continue | |
| } | |
| w.check(ident, v.Rhs[i]) | |
| } | |
| if ident, ok := lhs.(*ast.Ident); !ok || ident.Name == "_" { | |
| continue | |
| } | |
| w.check(ident, v.Rhs[i]) |
| if !ok { | ||
| return false | ||
| } | ||
| return named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == "time" && named.Obj().Name() == "Time" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| return named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == "time" && named.Obj().Name() == "Time" | |
| obj := named.Obj() | |
| pkg := obj.Pkg() | |
| return pkg != nil && pkg.Path() == "time" && obj.Name() == "Time" |
Fixes #1502
Summary
This PR introduces a new linting rule
confusing-epochthat enforces clear naming conventions for variables storing epoch time values. Variables initialized with epoch time methods (Unix(),UnixMilli(),UnixMicro(),UnixNano()) must have names that indicate their time unit to prevent confusion and potentialbugs.
Motivation
When working with epoch timestamps, it's easy to mix up time units (seconds vs milliseconds vs nanoseconds), leading to subtle bugs. For example:
This rule enforces that variable names clearly indicate the unit:
Changes Made