mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* feat(invariants): add the invariant-rule harness and the syncutil-locks rule * fix(ws,service): route the last five raw mutexes through syncutil The -tags deadlock CI pass only observes locks declared via syncutil, whose Mutex/RWMutex are build-tag aliases. These five were declared as raw sync types and were invisible to it, including the hub voice key-holder lock and the permission and role caches. TestServerInvariants now gates the tree against regressions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(invariants): walk the tree through os.Root to close a symlink TOCTOU gosec G122: reading a filepath.WalkDir-supplied path is race-prone, since a symlink swapped between the walk and the read escapes the intended tree. os.Root confines every read to the root and cannot be traversed out of. Walking the root's fs.FS also yields slash-separated paths already relative to it, so the filepath.Rel and ToSlash conversion is no longer needed. * fix(invariants): close syncutil-locks evasions, isolate per-rule tests, harden the gate - I1: TestServerInvariants now asserts every registered Rule.Scope directory exists and holds at least one non-test .go file, so the gate cannot pass by scanning nothing. - I2: split CheckSource into a thin wrapper over an unexported checkSourceWith(rules, ...), so TestSyncutilLocks tests the syncutil-locks rule in isolation instead of the whole registry. - I3: broaden checkSyncutilLocks to a single SelectorExpr match (any sync.Mutex/sync.RWMutex reference bound via f.Imports, aliases included) instead of only *ast.Field/*ast.ValueSpec. Catches := composite literals, untyped var specs, type aliases, and []sync.Mutex/map[K]sync.Mutex, none of which the old rule saw. A dot-import of "sync" is now its own violation, since it would otherwise let a bare Mutex evade the selector match entirely. - M2: suppression now keys off the violation's own Rule id (allowed[v.Line][v.Rule]) rather than the running rule's ID, so a rule that ever emits a sub-id isn't silently unsuppressible. - M3: Run sorts with sort.SliceStable, since an unreasoned allow comment and the violation it fails to suppress can share a file:line. - M4/M5/M1-partial: add a build-tag-gated fixture test, document that allow comments must be same-line, and correct the skipDirs comment to describe both the generated-code and gitignored-runtime-dir cases it actually covers. All ten original TestSyncutilLocks subtests pass unchanged; six new subtests cover the evasions above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(server): point at the syncutil-locks invariant gate Server/CLAUDE.md told developers not to hand-roll around syncutil but never said it's enforced. Note that Server/invariants/ checks it at go test time and that exceptions are greppable via grep -rn "invariant:allow" Server/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
207 lines
3.9 KiB
Go
207 lines
3.9 KiB
Go
package invariants
|
|
|
|
import (
|
|
"go/token"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSyncutilLocks(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
src string
|
|
want int
|
|
}{
|
|
{
|
|
name: "raw RWMutex field in ws is flagged",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
type Hub struct{ mu sync.RWMutex }
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "raw Mutex field in service is flagged",
|
|
path: "service/x.go",
|
|
src: `package service
|
|
import "sync"
|
|
type S struct{ mu sync.Mutex }
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "embedded raw Mutex is flagged",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
type Hub struct{ sync.Mutex }
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "local variable raw Mutex is flagged",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
func f() { var mu sync.Mutex; _ = mu }
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "syncutil alias is clean",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "github.com/owncord/server/syncutil"
|
|
type Hub struct{ mu syncutil.RWMutex }
|
|
`,
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "sync.Once and sync.WaitGroup are not locks",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
type Hub struct {
|
|
once sync.Once
|
|
wg sync.WaitGroup
|
|
}
|
|
`,
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "package outside scope is ignored",
|
|
path: "api/x.go",
|
|
src: `package api
|
|
import "sync"
|
|
type S struct{ mu sync.Mutex }
|
|
`,
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "allow comment with a reason suppresses",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
type Hub struct {
|
|
mu sync.Mutex //invariant:allow syncutil-locks — guards a cgo callback that needs a std lock
|
|
}
|
|
`,
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "allow comment without a reason does not suppress and is itself a violation",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
type Hub struct {
|
|
mu sync.Mutex //invariant:allow syncutil-locks
|
|
}
|
|
`,
|
|
want: 2,
|
|
},
|
|
{
|
|
name: "allow comment for a different rule does not suppress",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
type Hub struct {
|
|
mu sync.Mutex //invariant:allow some-other-rule — unrelated
|
|
}
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "short assignment composite literal is flagged",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
func f() {
|
|
mu := sync.Mutex{}
|
|
_ = mu
|
|
}
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "var with inferred type (nil ValueSpec.Type) is flagged",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
var mu = sync.Mutex{}
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "type alias to sync.Mutex is flagged",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
type m = sync.Mutex
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "aliased sync import is flagged",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import s "sync"
|
|
type Hub struct{ mu s.Mutex }
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "dot-import of sync is flagged as its own violation",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import . "sync"
|
|
var mu Mutex
|
|
`,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "slice-of-mutex composite type is flagged",
|
|
path: "ws/x.go",
|
|
src: `package ws
|
|
import "sync"
|
|
type Hub struct{ mus []sync.Mutex }
|
|
`,
|
|
want: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := checkSourceWith([]Rule{syncutilLocks}, token.NewFileSet(), tt.path, []byte(tt.src))
|
|
if len(got) != tt.want {
|
|
t.Fatalf("got %d violation(s), want %d:\n%v", len(got), tt.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSyncutilLocksMessageNamesTheFix(t *testing.T) {
|
|
src := `package ws
|
|
import "sync"
|
|
type Hub struct{ mu sync.RWMutex }
|
|
`
|
|
got := checkSourceWith([]Rule{syncutilLocks}, token.NewFileSet(), "ws/x.go", []byte(src))
|
|
if len(got) != 1 {
|
|
t.Fatalf("got %d violation(s), want 1: %v", len(got), got)
|
|
}
|
|
v := got[0]
|
|
if v.Rule != "syncutil-locks" {
|
|
t.Errorf("Rule = %q, want %q", v.Rule, "syncutil-locks")
|
|
}
|
|
if v.Line != 3 {
|
|
t.Errorf("Line = %d, want 3", v.Line)
|
|
}
|
|
if !strings.Contains(v.Msg, "syncutil.RWMutex") {
|
|
t.Errorf("message must name the fix, got %q", v.Msg)
|
|
}
|
|
if !strings.Contains(v.Msg, "-tags deadlock") {
|
|
t.Errorf("message must name the consequence, got %q", v.Msg)
|
|
}
|
|
}
|