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:
@@ -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 |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
145
rule/epoch_naming.go
Normal 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
11
test/epoch_naming_test.go
Normal 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
105
testdata/epoch_naming.go
vendored
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user