1
0
mirror of https://github.com/mgechev/revive.git synced 2025-11-25 22:12:38 +02:00

feature: new rule epoch-naming (#1556)

This commit is contained in:
Collie
2025-11-19 06:52:08 +00:00
committed by GitHub
parent dbcecde212
commit 48b4f67d2b
7 changed files with 305 additions and 1 deletions

View File

@@ -530,6 +530,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
| [`early-return`](./RULES_DESCRIPTIONS.md#early-return) | []string | Spots if-then-else statements where the predicate may be inverted to reduce nesting | no | no | | [`early-return`](./RULES_DESCRIPTIONS.md#early-return) | []string | Spots if-then-else statements where the predicate may be inverted to reduce nesting | no | no |
| [`empty-block`](./RULES_DESCRIPTIONS.md#empty-block) | n/a | Warns on empty code blocks | no | yes | | [`empty-block`](./RULES_DESCRIPTIONS.md#empty-block) | n/a | Warns on empty code blocks | no | yes |
| [`empty-lines`](./RULES_DESCRIPTIONS.md#empty-lines) | n/a | Warns when there are heading or trailing newlines in a block | no | no | | [`empty-lines`](./RULES_DESCRIPTIONS.md#empty-lines) | n/a | Warns when there are heading or trailing newlines in a block | no | no |
| [`epoch-naming`](./RULES_DESCRIPTIONS.md#epoch-naming) | n/a | Enforces naming conventions for epoch time variables | no | yes |
| [`enforce-map-style`](./RULES_DESCRIPTIONS.md#enforce-map-style) | string (defaults to "any") | Enforces consistent usage of `make(map[type]type)` or `map[type]type{}` for map initialization. Does not affect `make(map[type]type, size)` constructions. | no | no | | [`enforce-map-style`](./RULES_DESCRIPTIONS.md#enforce-map-style) | string (defaults to "any") | Enforces consistent usage of `make(map[type]type)` or `map[type]type{}` for map initialization. Does not affect `make(map[type]type, size)` constructions. | no | no |
| [`enforce-repeated-arg-type-style`](./RULES_DESCRIPTIONS.md#enforce-repeated-arg-type-style) | string (defaults to "any") | Enforces consistent style for repeated argument and/or return value types. | no | no | | [`enforce-repeated-arg-type-style`](./RULES_DESCRIPTIONS.md#enforce-repeated-arg-type-style) | string (defaults to "any") | Enforces consistent style for repeated argument and/or return value types. | no | no |
| [`enforce-slice-style`](./RULES_DESCRIPTIONS.md#enforce-slice-style) | string (defaults to "any") | Enforces consistent usage of `make([]type, 0)` or `[]type{}` for slice initialization. Does not affect `make(map[type]type, non_zero_len, or_non_zero_cap)` constructions. | no | no | | [`enforce-slice-style`](./RULES_DESCRIPTIONS.md#enforce-slice-style) | string (defaults to "any") | Enforces consistent usage of `make([]type, 0)` or `[]type{}` for slice initialization. Does not affect `make(map[type]type, non_zero_len, or_non_zero_cap)` constructions. | no | no |

View File

@@ -29,6 +29,7 @@ List of all available rules.
- [early-return](#early-return) - [early-return](#early-return)
- [empty-block](#empty-block) - [empty-block](#empty-block)
- [empty-lines](#empty-lines) - [empty-lines](#empty-lines)
- [epoch-naming](#epoch-naming)
- [enforce-map-style](#enforce-map-style) - [enforce-map-style](#enforce-map-style)
- [enforce-repeated-arg-type-style](#enforce-repeated-arg-type-style) - [enforce-repeated-arg-type-style](#enforce-repeated-arg-type-style)
- [enforce-slice-style](#enforce-slice-style) - [enforce-slice-style](#enforce-slice-style)
@@ -498,6 +499,46 @@ this rule warns when there are heading or trailing newlines in code blocks.
_Configuration_: N/A _Configuration_: N/A
## epoch-naming
_Description_: Variables initialized with epoch time methods (`time.Now().Unix()`, `time.Now().UnixMilli()`,
`time.Now().UnixMicro()`, `time.Now().UnixNano()`) should have names that clearly indicate their time unit to
prevent confusion and potential bugs when working with different time scales.
This rule enforces that variable names contain appropriate suffixes based on the method used:
- `Unix()`: variable name should end with "Sec", "Second" or "Seconds"
- `UnixMilli()`: variable name should end with "Milli" or "Ms"
- `UnixMicro()`: variable name should end with "Micro", "Microsecond", "Microseconds" or "Us"
- `UnixNano()`: variable name should end with "Nano" or "Ns"
The rule checks variable declarations, short variable declarations (`:=`), and regular assignments (`=`).
The suffix matching is case-insensitive and must appear at the end of the variable name.
### Examples (epoch-naming)
Before (violation):
```go
timestamp := time.Now().Unix() // unclear which unit
createdAt := time.Now().UnixMilli() // missing unit indicator
t := time.Now().UnixNano() // lacks required suffix
```
After (fixed):
```go
timestampSec := time.Now().Unix() // clearly seconds
createdAtMs := time.Now().UnixMilli() // clearly milliseconds
tNano := time.Now().UnixNano() // clearly nanoseconds
// Alternative valid names
createdSeconds := time.Now().Unix() // full word is fine
updatedMicro := time.Now().UnixMicro() // microseconds
```
_Configuration_: N/A
## enforce-map-style ## enforce-map-style
_Description_: This rule enforces consistent usage of `make(map[type]type)` or `map[type]type{}` for map initialization. _Description_: This rule enforces consistent usage of `make(map[type]type)` or `map[type]type{}` for map initialization.

View File

@@ -116,6 +116,7 @@ var allRules = append([]lint.Rule{
&rule.InefficientMapLookupRule{}, &rule.InefficientMapLookupRule{},
&rule.ForbiddenCallInWgGoRule{}, &rule.ForbiddenCallInWgGoRule{},
&rule.UnnecessaryIfRule{}, &rule.UnnecessaryIfRule{},
&rule.EpochNamingRule{},
}, defaultRules...) }, defaultRules...)
// allFormatters is a list of all available formatters to output the linting results. // allFormatters is a list of all available formatters to output the linting results.

View File

@@ -438,7 +438,7 @@ func TestGetLintingRules(t *testing.T) {
// len of defaultRules // len of defaultRules
defaultRulesCount = 23 defaultRulesCount = 23
// len of allRules: update this when adding new rules // len of allRules: update this when adding new rules
allRulesCount = 99 allRulesCount = 100
) )
tt := map[string]struct { tt := map[string]struct {

145
rule/epoch_naming.go Normal file
View File

@@ -0,0 +1,145 @@
package rule
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
"github.com/mgechev/revive/lint"
)
// EpochNamingRule lints epoch time variable naming.
type EpochNamingRule struct{}
// Apply applies the rule to given file.
func (*EpochNamingRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
var failures []lint.Failure
walker := lintEpochNaming{
file: file,
onFailure: func(failure lint.Failure) {
failures = append(failures, failure)
},
}
file.Pkg.TypeCheck()
ast.Walk(walker, file.AST)
return failures
}
// Name returns the rule name.
func (*EpochNamingRule) Name() string {
return "epoch-naming"
}
type lintEpochNaming struct {
file *lint.File
onFailure func(lint.Failure)
}
var epochUnits = map[string][]string{
"Unix": {"Sec", "Second", "Seconds"},
"UnixMilli": {"Milli", "Ms"},
"UnixMicro": {"Micro", "Microsecond", "Microseconds", "Us"},
"UnixNano": {"Nano", "Ns"},
}
func (w lintEpochNaming) Visit(node ast.Node) ast.Visitor {
switch v := node.(type) {
case *ast.ValueSpec:
// Handle var declarations
valuesLen := len(v.Values)
for i, name := range v.Names {
if i >= valuesLen {
break
}
w.check(name, v.Values[i])
}
case *ast.AssignStmt:
// Handle both short variable declarations (:=) and regular assignments (=)
if v.Tok != token.DEFINE && v.Tok != token.ASSIGN {
return w
}
rhsLen := len(v.Rhs)
for i, lhs := range v.Lhs {
if i >= rhsLen {
break
}
ident, ok := lhs.(*ast.Ident)
if !ok || ident.Name == "_" {
continue
}
w.check(ident, v.Rhs[i])
}
}
return w
}
func (w lintEpochNaming) check(name *ast.Ident, value ast.Expr) {
call, ok := value.(*ast.CallExpr)
if !ok {
return
}
selector, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return
}
// Check if the receiver is of type time.Time
receiverType := w.file.Pkg.TypeOf(selector.X)
if receiverType == nil {
return
}
if !isTime(receiverType) {
return
}
methodName := selector.Sel.Name
suffixes, ok := epochUnits[methodName]
if !ok {
return
}
varName := name.Name
if !hasAnySuffix(varName, suffixes) {
w.onFailure(lint.Failure{
Confidence: 0.9,
Node: name,
Category: lint.FailureCategoryNaming,
Failure: fmt.Sprintf("var %s should have one of these suffixes: %s", varName, strings.Join(suffixes, ", ")),
})
}
}
func isTime(typ types.Type) bool {
named, ok := typ.(*types.Named)
if !ok {
return false
}
obj := named.Obj()
if obj == nil {
return false
}
pkg := obj.Pkg()
return pkg != nil && pkg.Path() == "time" && obj.Name() == "Time"
}
func hasAnySuffix(s string, suffixes []string) bool {
lowerName := strings.ToLower(s)
for _, suffix := range suffixes {
if strings.HasSuffix(lowerName, strings.ToLower(suffix)) {
return true
}
}
return false
}

11
test/epoch_naming_test.go Normal file
View File

@@ -0,0 +1,11 @@
package test_test
import (
"testing"
"github.com/mgechev/revive/rule"
)
func TestEpochNaming(t *testing.T) {
testRule(t, "epoch_naming", &rule.EpochNamingRule{})
}

105
testdata/epoch_naming.go vendored Normal file
View File

@@ -0,0 +1,105 @@
package testdata
import "time"
var (
creation = time.Now().Unix() // MATCH /var creation should have one of these suffixes: Sec, Second, Seconds/
creationSeconds = time.Now().Unix()
createdAtSec = time.Now().Unix()
loginTimeMilli = time.Now().UnixMilli()
m = time.Now().UnixMilli() // MATCH /var m should have one of these suffixes: Milli, Ms/
t = time.Now().UnixNano() // MATCH /var t should have one of these suffixes: Nano, Ns/
tNano = time.Now().UnixNano()
epochNano = time.Now().UnixNano()
// Very short but valid names
sec = time.Now().Unix()
ns = time.Now().UnixNano()
ms = time.Now().UnixMilli()
us = time.Now().UnixMicro()
timestampNs = time.Now().UnixNano()
createdMs = time.Now().UnixMilli()
timeSecond = time.Now().Unix()
// Edge case 1: UnixMicro() support (Go 1.17+)
timestampMicro = time.Now().UnixMicro()
timestampUs = time.Now().UnixMicro()
timestampMicrosecond = time.Now().UnixMicro()
timestampMicroseconds = time.Now().UnixMicro()
badMicroValue = time.Now().UnixMicro() // MATCH /var badMicroValue should have one of these suffixes: Micro, Microsecond, Microseconds, Us/
invalidUsValue = time.Now().UnixMicro() // MATCH /var invalidUsValue should have one of these suffixes: Micro, Microsecond, Microseconds, Us/
// Wrong unit suffix - using suffix for different time unit
badMs = time.Now().Unix() // MATCH /var badMs should have one of these suffixes: Sec, Second, Seconds/
wrongSec = time.Now().UnixMilli() // MATCH /var wrongSec should have one of these suffixes: Milli, Ms/
wrongNano = time.Now().Unix() // MATCH /var wrongNano should have one of these suffixes: Sec, Second, Seconds/
confusedMicro = time.Now().UnixNano() // MATCH /var confusedMicro should have one of these suffixes: Nano, Ns/
// Edge case 3: Case variations (mixed case)
timeSEC = time.Now().Unix() // OK - SEC is valid suffix
timeNANO = time.Now().UnixNano() // OK - NANO is valid suffix
timeMILLI = time.Now().UnixMilli() // OK - MILLI is valid suffix
timeMICRO = time.Now().UnixMicro() // OK - MICRO is valid suffix
timeSec = time.Now().Unix() // OK - Sec is valid suffix
timeSeconds = time.Now().Unix() // OK - Seconds is valid suffix
// Edge case 6a: Explicit type declarations
badExplicit int64 = time.Now().Unix() // MATCH /var badExplicit should have one of these suffixes: Sec, Second, Seconds/
goodSecExplicit int64 = time.Now().Unix() // MATCH /var goodSecExplicit should have one of these suffixes: Sec, Second, Seconds/
explicitSec int64 = time.Now().Unix()
explicitSeconds int64 = time.Now().Unix()
// Edge case 6b: Suffix in middle vs end (suffix must be at the end)
secInMiddle int64 = time.Now().Unix() // MATCH /var secInMiddle should have one of these suffixes: Sec, Second, Seconds/
nanoInMiddle int64 = time.Now().UnixNano() // MATCH /var nanoInMiddle should have one of these suffixes: Nano, Ns/
)
func foo() {
anotherInvalid := time.Now().Unix() // MATCH /var anotherInvalid should have one of these suffixes: Sec, Second, Seconds/
anotherValidSec := time.Now().Unix()
_ = time.Now().Unix() // assignment to blank identifier - ignored
_ := time.Now().Unix() // short declaration with blank identifier - should be ignored
println(anotherInvalid, anotherValidSec)
// Edge cases that should NOT be flagged (no variable declaration)
bar(time.Now().Unix()) // function argument - OK
_ = []int64{time.Now().UnixMilli()} // slice literal - OK
baz()
// Edge case 2: Multiple variables in one statement
invalidTime := time.Now().Unix() // MATCH /var invalidTime should have one of these suffixes: Sec, Second, Seconds/
invalidMilliTime := time.Now().UnixMilli() // MATCH /var invalidMilliTime should have one of these suffixes: Milli, Ms/
goodSec, goodMs := time.Now().Unix(), time.Now().UnixMilli() // Both should pass
badNanoValue := time.Now().UnixNano() // MATCH /var badNanoValue should have one of these suffixes: Nano, Ns/
badMicroValue2 := time.Now().UnixMicro() // MATCH /var badMicroValue2 should have one of these suffixes: Micro, Microsecond, Microseconds, Us/
goodNs, goodUs := time.Now().UnixNano(), time.Now().UnixMicro() // Both should pass
println(invalidTime, invalidMilliTime, goodSec, goodMs, badNanoValue, badMicroValue2, goodNs, goodUs)
}
func bar(input int64) {}
func baz() int64 {
return time.Now().UnixNano() // return statement - OK
}
// Struct fields should be checked
type Config struct {
timestamp int64
}
func structTest() {
c := Config{timestamp: time.Now().Unix()} // struct field - OK (no variable for the timestamp value itself)
}
// Regular assignment (=) should be checked
func regularAssignment() {
var x int64 // declaration without initialization - OK
x = time.Now().Unix() // MATCH /var x should have one of these suffixes: Sec, Second, Seconds/
println(x)
}
// Edge case 5: Compound assignments
func compoundAssignment() {
var counter int64
counter += time.Now().Unix() // Compound assignment - currently not checked but documented
println(counter)
}