Skip to content

Commit 1231047

Browse files
committed
feat: add TestNoRawTimeFormats audit check
Catches raw Go time layout strings in time.Parse, time.Format, and time.AppendFormat calls. All layouts must use config/time constants. Zero violations found; pure guardrail. Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent 5c0bf3a commit 1231047

1 file changed

Lines changed: 134 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package audit
8+
9+
import (
10+
"go/ast"
11+
"go/token"
12+
"strconv"
13+
"strings"
14+
"testing"
15+
)
16+
17+
// timeFormatFuncs lists time package methods that take
18+
// a layout string argument.
19+
var timeFormatFuncs = map[string]bool{
20+
"Parse": true,
21+
"Format": true,
22+
"AppendFormat": true,
23+
}
24+
25+
// goRefTime matches substrings of Go's reference time
26+
// (Mon Jan 2 15:04:05 MST 2006) that indicate a raw
27+
// time layout string.
28+
var goRefTimeHints = []string{
29+
"2006", "01", "02", "15", "04", "05",
30+
"Jan", "Mon", "MST",
31+
}
32+
33+
// TestNoRawTimeFormats ensures raw time layout strings
34+
// in time.Parse, time.Format, and time.AppendFormat
35+
// calls use config/time constants instead of inline
36+
// format strings.
37+
//
38+
// Stdlib constants (time.RFC3339, etc.) are exempt
39+
// because they are already named.
40+
//
41+
// The definition site (internal/config/time/) is exempt.
42+
// Test files are exempt.
43+
//
44+
// See specs/ast-audit-tests.md for rationale.
45+
func TestNoRawTimeFormats(t *testing.T) {
46+
pkgs := loadPackages(t)
47+
var violations []string
48+
49+
for _, pkg := range pkgs {
50+
if strings.Contains(pkg.PkgPath, "config/time") {
51+
continue
52+
}
53+
54+
for _, file := range pkg.Syntax {
55+
fpath := pkg.Fset.Position(file.Pos()).Filename
56+
if isTestFile(fpath) {
57+
continue
58+
}
59+
60+
ast.Inspect(file, func(n ast.Node) bool {
61+
call, ok := n.(*ast.CallExpr)
62+
if !ok {
63+
return true
64+
}
65+
66+
sel, ok := call.Fun.(*ast.SelectorExpr)
67+
if !ok {
68+
return true
69+
}
70+
71+
if !timeFormatFuncs[sel.Sel.Name] {
72+
return true
73+
}
74+
75+
// Find the layout argument: first arg
76+
// for Parse, first arg for Format/
77+
// AppendFormat (method on time.Time).
78+
var layoutArg ast.Expr
79+
switch sel.Sel.Name {
80+
case "Parse":
81+
// time.Parse(layout, value)
82+
if len(call.Args) >= 1 {
83+
layoutArg = call.Args[0]
84+
}
85+
case "Format", "AppendFormat":
86+
// t.Format(layout)
87+
if len(call.Args) >= 1 {
88+
layoutArg = call.Args[0]
89+
}
90+
}
91+
92+
if layoutArg == nil {
93+
return true
94+
}
95+
96+
lit, ok := layoutArg.(*ast.BasicLit)
97+
if !ok || lit.Kind != token.STRING {
98+
return true
99+
}
100+
101+
s, err := strconv.Unquote(lit.Value)
102+
if err != nil {
103+
return true
104+
}
105+
106+
if looksLikeTimeLayout(s) {
107+
violations = append(violations,
108+
posString(pkg.Fset, lit.Pos())+
109+
": raw time format "+
110+
lit.Value+
111+
" — use config/time",
112+
)
113+
}
114+
115+
return true
116+
})
117+
}
118+
}
119+
120+
for _, v := range violations {
121+
t.Error(v)
122+
}
123+
}
124+
125+
// looksLikeTimeLayout reports whether s contains
126+
// Go reference time components.
127+
func looksLikeTimeLayout(s string) bool {
128+
for _, hint := range goRefTimeHints {
129+
if strings.Contains(s, hint) {
130+
return true
131+
}
132+
}
133+
return false
134+
}

0 commit comments

Comments
 (0)