mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
lint: detect unnecessarily bidirectional channels
This commit is contained in:
19 files changed
+957
-22
No files matched your search
@@ -114,6 +114,19 @@ opt-in cops are currently selected. `Lint/ContextFirstParameter` and
|
||||
intentionally owned lifecycle or telemetry contexts would require suppressions or
|
||||
unrelated refactoring.
|
||||
|
||||
`Lint/ChannelDirection` suggests directional raw-channel parameters and results.
|
||||
It follows requirements through locals, fields, direct calls, returns, and closure
|
||||
captures, including internal/external tests. Closing requires send capability;
|
||||
receiving and closing requires both. Connected declarations may need coordinated
|
||||
changes, so there is no autofix. Generated and frozen declarations retain their
|
||||
original contracts. Unknown calls, interface escapes, reflective type arguments,
|
||||
function values, and unsupported structural conversions conservatively retain
|
||||
capabilities. Tuple flows preserve both channel operations and aggregate escapes.
|
||||
The check assumes the loaded program is the consumer: review exported API changes
|
||||
for downstream users and validate other build targets. It covers the active build,
|
||||
not every build-tag or platform variant; it does not prove channel ownership or
|
||||
send/close race safety.
|
||||
|
||||
`Lint/HTTPRequestWithContext` enforces contextual HTTP request construction outside
|
||||
frozen config versions. It inspects production files only; matching is syntactic,
|
||||
so aliases and shadowing require care. `Lint/NoFatalOutsideMain` reserves
|
||||
|
||||
@@ -0,0 +1,720 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/dgageot/rubocop-go/cop"
|
||||
"github.com/dgageot/rubocop-go/prog"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
// ChannelDirection follows channel capabilities backwards through assignments,
|
||||
// fields, calls and returns. It suggests coordinated type changes, not autofixes.
|
||||
// Unknown uses retain both capabilities; only the active build is analyzed.
|
||||
var ChannelDirection = prog.New(cop.Meta{
|
||||
Name: "Lint/ChannelDirection",
|
||||
Description: "use directional channels when only one capability is required",
|
||||
Severity: cop.Convention,
|
||||
}, checkChannelDirection)
|
||||
|
||||
func checkChannelDirection(p *prog.Pass) {
|
||||
var dirs []string
|
||||
for _, pkg := range p.Program.Packages {
|
||||
if pkg.Dir != "" {
|
||||
dirs = append(dirs, pkg.Dir)
|
||||
}
|
||||
}
|
||||
if len(dirs) == 0 {
|
||||
return
|
||||
}
|
||||
pkgs, err := packages.Load(&packages.Config{
|
||||
Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles |
|
||||
packages.NeedImports | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax,
|
||||
Dir: dirs[0], Fset: p.Program.Fset, Tests: true,
|
||||
}, dirs...)
|
||||
if err != nil {
|
||||
p.Reportf(token.NoPos, "cannot inspect channel directions: %v", err)
|
||||
return
|
||||
}
|
||||
analyzeChannelDirections(p, pkgs)
|
||||
}
|
||||
|
||||
type channelCapabilities uint8
|
||||
|
||||
const (
|
||||
channelSend channelCapabilities = 1 << iota
|
||||
channelReceive
|
||||
channelBoth = channelSend | channelReceive
|
||||
)
|
||||
|
||||
type channelSlot struct {
|
||||
needs channelCapabilities
|
||||
sources map[*channelSlot]bool
|
||||
pos token.Pos
|
||||
end token.Pos
|
||||
label string
|
||||
}
|
||||
|
||||
type channelValue struct {
|
||||
slot *channelSlot
|
||||
typ types.Type
|
||||
}
|
||||
|
||||
type channelFunction struct {
|
||||
params, results []*channelSlot
|
||||
resultTypes []types.Type
|
||||
}
|
||||
|
||||
type channelAnalysis struct {
|
||||
fset *token.FileSet
|
||||
slots map[string]*channelSlot
|
||||
objects map[types.Object]*channelSlot
|
||||
functions map[string]*channelFunction
|
||||
info *types.Info
|
||||
direct map[ast.Expr]bool
|
||||
interfaces map[string][]*types.Signature
|
||||
}
|
||||
|
||||
func analyzeChannelDirections(p *prog.Pass, pkgs []*packages.Package) {
|
||||
a := &channelAnalysis{
|
||||
fset: p.Program.Fset, slots: make(map[string]*channelSlot),
|
||||
objects: make(map[types.Object]*channelSlot), functions: make(map[string]*channelFunction),
|
||||
direct: make(map[ast.Expr]bool), interfaces: make(map[string][]*types.Signature),
|
||||
}
|
||||
visited := make(map[*types.Package]bool)
|
||||
var interfaces func(*types.Package)
|
||||
interfaces = func(pkg *types.Package) {
|
||||
if pkg == nil || visited[pkg] {
|
||||
return
|
||||
}
|
||||
visited[pkg] = true
|
||||
for _, name := range pkg.Scope().Names() {
|
||||
if obj, ok := pkg.Scope().Lookup(name).(*types.TypeName); ok {
|
||||
if iface, ok := obj.Type().Underlying().(*types.Interface); ok {
|
||||
for method := range iface.Complete().Methods() {
|
||||
sig := method.Type().(*types.Signature)
|
||||
a.interfaces[method.Name()] = append(a.interfaces[method.Name()], sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, dep := range pkg.Imports() {
|
||||
interfaces(dep)
|
||||
}
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
interfaces(pkg.Types)
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
a.info = pkg.TypesInfo
|
||||
if a.info == nil {
|
||||
continue
|
||||
}
|
||||
for _, file := range pkg.Syntax {
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if expr, ok := n.(ast.Expr); ok {
|
||||
if typ := a.info.TypeOf(expr); typ != nil {
|
||||
if iface, ok := typ.Underlying().(*types.Interface); ok {
|
||||
for method := range iface.Complete().Methods() {
|
||||
a.interfaces[method.Name()] = append(a.interfaces[method.Name()], method.Type().(*types.Signature))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
// Keep all test variants: they contribute constraints to the same source slots.
|
||||
for _, pkg := range pkgs {
|
||||
a.info = pkg.TypesInfo
|
||||
if a.info == nil {
|
||||
continue
|
||||
}
|
||||
for _, file := range pkg.Syntax {
|
||||
editable := !pkg.IllTyped && !ast.IsGenerated(file) && !frozenConfigPath.MatchString(filepath.ToSlash(a.fset.PositionFor(file.Pos(), false).Filename))
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
switch n := n.(type) {
|
||||
case *ast.Ident:
|
||||
if obj, ok := a.info.Defs[n].(*types.Var); ok {
|
||||
a.variable(obj, editable)
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
obj, _ := a.info.Defs[n.Name].(*types.Func)
|
||||
if obj != nil {
|
||||
sig := obj.Type().(*types.Signature)
|
||||
a.registerFunction(a.key(obj.Pos()), sig, n.Type, editable && n.Body != nil)
|
||||
if sig.Recv() != nil {
|
||||
for _, iface := range a.interfaces[obj.Name()] {
|
||||
if sig.Recv() == nil || types.Identical(sig.Params(), iface.Params()) && types.Identical(sig.Results(), iface.Results()) {
|
||||
a.freezeFunction(a.functions[a.key(obj.Pos())])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.FuncLit:
|
||||
if typ := a.info.TypeOf(n); typ != nil {
|
||||
a.registerFunction(a.key(n.Pos()), typ.(*types.Signature), n.Type, editable)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
a.info = pkg.TypesInfo
|
||||
if a.info == nil {
|
||||
continue
|
||||
}
|
||||
// Generic function values can hide type arguments absent from their signatures.
|
||||
for _, instantiation := range a.info.Instances {
|
||||
for typ := range instantiation.TypeArgs.Types() {
|
||||
a.escapeType(typ, make(map[types.Type]bool))
|
||||
}
|
||||
}
|
||||
for _, file := range pkg.Syntax {
|
||||
a.inspect(file, nil)
|
||||
}
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
if pkg.IllTyped {
|
||||
return
|
||||
} // Do not infer capabilities from incomplete uses.
|
||||
}
|
||||
// Each bit is added at most once per slot, including recursive call cycles.
|
||||
queue := make([]*channelSlot, 0, len(a.slots))
|
||||
for _, slot := range a.slots {
|
||||
if slot.needs != 0 {
|
||||
queue = append(queue, slot)
|
||||
}
|
||||
}
|
||||
for len(queue) > 0 {
|
||||
slot := queue[0]
|
||||
queue = queue[1:]
|
||||
for source := range slot.sources {
|
||||
if source.needs|slot.needs != source.needs {
|
||||
source.needs |= slot.needs
|
||||
queue = append(queue, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
var findings []*channelSlot
|
||||
for _, slot := range a.slots {
|
||||
if slot.label != "" && (slot.needs == channelSend || slot.needs == channelReceive) {
|
||||
findings = append(findings, slot)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(findings, func(x, y *channelSlot) int { return int(x.pos - y.pos) })
|
||||
for _, slot := range findings {
|
||||
direction := "send-only (chan<- T)"
|
||||
if slot.needs == channelReceive {
|
||||
direction = "receive-only (<-chan T)"
|
||||
}
|
||||
p.ReportAtf(slot.pos, slot.end, "%s can be %s; linked channel declarations may need narrowing too", slot.label, direction)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) key(pos token.Pos) string {
|
||||
p := a.fset.PositionFor(pos, false)
|
||||
return fmt.Sprintf("%s:%d", p.Filename, p.Offset)
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) slot(key string) *channelSlot {
|
||||
if a.slots[key] == nil {
|
||||
a.slots[key] = &channelSlot{sources: make(map[*channelSlot]bool)}
|
||||
}
|
||||
return a.slots[key]
|
||||
}
|
||||
|
||||
func channelType(t types.Type) *types.Chan {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
ch, _ := types.Unalias(t).Underlying().(*types.Chan)
|
||||
return ch
|
||||
}
|
||||
|
||||
func fixedChannelNeeds(t types.Type) channelCapabilities {
|
||||
if ch := channelType(t); ch != nil {
|
||||
switch ch.Dir() {
|
||||
case types.SendOnly:
|
||||
return channelSend
|
||||
case types.RecvOnly:
|
||||
return channelReceive
|
||||
}
|
||||
}
|
||||
return channelBoth
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) variable(obj types.Object, editable bool) *channelSlot {
|
||||
if obj == nil || channelType(obj.Type()) == nil {
|
||||
return nil
|
||||
}
|
||||
if slot := a.objects[obj]; slot != nil {
|
||||
return slot
|
||||
}
|
||||
slot := a.slot(a.key(obj.Pos()))
|
||||
a.objects[obj] = slot
|
||||
_, raw := obj.Type().(*types.Chan)
|
||||
if !editable || !raw || channelType(obj.Type()).Dir() != types.SendRecv {
|
||||
slot.needs |= fixedChannelNeeds(obj.Type())
|
||||
}
|
||||
return slot
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) registerFunction(key string, sig *types.Signature, syntax *ast.FuncType, editable bool) {
|
||||
fn := a.functions[key]
|
||||
if fn == nil {
|
||||
fn = &channelFunction{}
|
||||
a.functions[key] = fn
|
||||
}
|
||||
register := func(tuple *types.Tuple, fields *ast.FieldList, kind string) []*channelSlot {
|
||||
slots := make([]*channelSlot, tuple.Len())
|
||||
index := 0
|
||||
if fields == nil {
|
||||
return slots
|
||||
}
|
||||
for _, field := range fields.List {
|
||||
for j := range max(1, len(field.Names)) {
|
||||
obj := tuple.At(index)
|
||||
if ch := channelType(obj.Type()); ch != nil {
|
||||
slot := a.slot(fmt.Sprintf("%s:%s:%d", key, kind, index))
|
||||
a.objects[obj] = slot
|
||||
slots[index] = slot
|
||||
_, raw := field.Type.(*ast.ChanType)
|
||||
if !editable || !raw || ch.Dir() != types.SendRecv {
|
||||
slot.needs |= fixedChannelNeeds(obj.Type())
|
||||
} else {
|
||||
slot.pos, slot.end = field.Type.Pos(), field.Type.End()
|
||||
slot.label = kind
|
||||
if len(field.Names) > 0 {
|
||||
slot.pos, slot.end = field.Names[j].Pos(), field.Names[j].End()
|
||||
slot.label += " " + field.Names[j].Name
|
||||
}
|
||||
}
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
return slots
|
||||
}
|
||||
fn.params = register(sig.Params(), syntax.Params, "parameter")
|
||||
fn.results = register(sig.Results(), syntax.Results, "result")
|
||||
fn.resultTypes = nil
|
||||
for v := range sig.Results().Variables() {
|
||||
fn.resultTypes = append(fn.resultTypes, v.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) object(expr ast.Expr) types.Object {
|
||||
switch expr := ast.Unparen(expr).(type) {
|
||||
case *ast.Ident:
|
||||
return a.info.ObjectOf(expr)
|
||||
case *ast.SelectorExpr:
|
||||
return a.info.ObjectOf(expr.Sel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) function(expr ast.Expr) *channelFunction {
|
||||
expr = ast.Unparen(expr)
|
||||
if fn, ok := expr.(*ast.FuncLit); ok {
|
||||
return a.functions[a.key(fn.Pos())]
|
||||
}
|
||||
if obj, ok := a.object(expr).(*types.Func); ok {
|
||||
return a.functions[a.key(obj.Origin().Pos())]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) value(expr ast.Expr) *channelSlot {
|
||||
if expr == nil || channelType(a.info.TypeOf(expr)) == nil {
|
||||
return nil
|
||||
}
|
||||
expr = ast.Unparen(expr)
|
||||
if obj := a.object(expr); obj != nil {
|
||||
if slot := a.objects[obj]; slot != nil {
|
||||
return slot
|
||||
}
|
||||
return a.variable(obj, false)
|
||||
}
|
||||
if call, ok := expr.(*ast.CallExpr); ok {
|
||||
if fn := a.function(call.Fun); fn != nil && len(fn.results) == 1 {
|
||||
return fn.results[0]
|
||||
}
|
||||
}
|
||||
return a.slot(a.key(expr.Pos()) + ":value")
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) destination(expr ast.Expr) *channelSlot {
|
||||
switch ast.Unparen(expr).(type) {
|
||||
case *ast.Ident, *ast.SelectorExpr:
|
||||
return a.value(expr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) markDirect(expr ast.Expr) {
|
||||
expr = ast.Unparen(expr)
|
||||
a.direct[expr] = true
|
||||
if sel, ok := expr.(*ast.SelectorExpr); ok {
|
||||
a.direct[sel.Sel] = true
|
||||
}
|
||||
}
|
||||
|
||||
func channelFlow(source, dest *channelSlot) {
|
||||
if source != nil && dest != nil && source != dest {
|
||||
dest.sources[source] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) require(expr ast.Expr, needs channelCapabilities) {
|
||||
if slot := a.value(expr); slot != nil {
|
||||
slot.needs |= needs
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) bind(expr ast.Expr, dest *channelSlot, typ types.Type) {
|
||||
a.bindValue(channelValue{a.value(expr), a.info.TypeOf(expr)}, dest, typ)
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) bindValue(source channelValue, dest *channelSlot, typ types.Type) {
|
||||
if source.slot != nil {
|
||||
if dest != nil {
|
||||
channelFlow(source.slot, dest)
|
||||
} else {
|
||||
source.slot.needs |= fixedChannelNeeds(typ)
|
||||
a.escapeType(source.typ, make(map[types.Type]bool))
|
||||
}
|
||||
return
|
||||
}
|
||||
if source.typ == nil {
|
||||
return
|
||||
}
|
||||
underlying := types.Unalias(source.typ)
|
||||
if ptr, ok := underlying.(*types.Pointer); ok {
|
||||
underlying = types.Unalias(ptr.Elem())
|
||||
}
|
||||
_, anonymous := underlying.(*types.Struct)
|
||||
iface := isChannelInterface(typ)
|
||||
if anonymous || iface || !types.Identical(source.typ, typ) {
|
||||
a.escapeType(source.typ, make(map[types.Type]bool))
|
||||
}
|
||||
}
|
||||
|
||||
func isChannelInterface(typ types.Type) bool {
|
||||
if typ == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := typ.Underlying().(*types.Interface)
|
||||
return ok
|
||||
}
|
||||
|
||||
// values flattens tuple expressions without dropping aggregate result types.
|
||||
func (a *channelAnalysis) values(exprs []ast.Expr) []channelValue {
|
||||
if len(exprs) == 1 {
|
||||
if tuple, ok := a.info.TypeOf(exprs[0]).(*types.Tuple); ok {
|
||||
values := make([]channelValue, tuple.Len())
|
||||
var fn *channelFunction
|
||||
if call, ok := ast.Unparen(exprs[0]).(*ast.CallExpr); ok {
|
||||
fn = a.function(call.Fun)
|
||||
}
|
||||
for i := range tuple.Len() {
|
||||
values[i].typ = tuple.At(i).Type()
|
||||
if fn != nil && i < len(fn.results) {
|
||||
values[i].slot = fn.results[i]
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
||||
values := make([]channelValue, len(exprs))
|
||||
for i, expr := range exprs {
|
||||
values[i] = channelValue{a.value(expr), a.info.TypeOf(expr)}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// inspect owns traversal so each channel use is classified, including closure captures.
|
||||
func (a *channelAnalysis) inspect(n ast.Node, fn *channelFunction) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
switch n := n.(type) {
|
||||
case *ast.FuncDecl:
|
||||
obj, _ := a.info.Defs[n.Name].(*types.Func)
|
||||
if obj != nil && n.Body != nil {
|
||||
a.inspect(n.Body, a.functions[a.key(obj.Pos())])
|
||||
}
|
||||
return
|
||||
case *ast.FuncLit:
|
||||
if !a.direct[n] {
|
||||
a.freezeFunction(a.function(n))
|
||||
}
|
||||
a.inspect(n.Body, a.functions[a.key(n.Pos())])
|
||||
return
|
||||
case *ast.Ident, *ast.SelectorExpr:
|
||||
if expr, ok := n.(ast.Expr); ok && !a.direct[expr] {
|
||||
a.freezeFunction(a.function(expr))
|
||||
if typ := a.info.TypeOf(expr); typ != nil {
|
||||
if _, ok := typ.Underlying().(*types.Signature); ok {
|
||||
a.escapeType(typ, make(map[types.Type]bool))
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.FuncType, *ast.ChanType:
|
||||
return
|
||||
case *ast.AssignStmt:
|
||||
a.assign(n.Lhs, n.Rhs)
|
||||
case *ast.ValueSpec:
|
||||
lhs := make([]ast.Expr, len(n.Names))
|
||||
for i, name := range n.Names {
|
||||
lhs[i] = name
|
||||
}
|
||||
a.assign(lhs, n.Values)
|
||||
case *ast.ReturnStmt:
|
||||
if fn != nil {
|
||||
a.bindResults(n.Results, fn)
|
||||
}
|
||||
case *ast.SendStmt:
|
||||
a.require(n.Chan, channelSend)
|
||||
ch := channelType(a.info.TypeOf(n.Chan))
|
||||
if ch != nil {
|
||||
a.bind(n.Value, nil, ch.Elem())
|
||||
}
|
||||
case *ast.RangeStmt:
|
||||
a.require(n.X, channelReceive)
|
||||
case *ast.UnaryExpr:
|
||||
switch n.Op {
|
||||
case token.ARROW:
|
||||
a.require(n.X, channelReceive)
|
||||
case token.AND:
|
||||
a.require(n.X, channelBoth)
|
||||
}
|
||||
case *ast.BinaryExpr:
|
||||
a.compare(n.X, n.Y)
|
||||
case *ast.SwitchStmt:
|
||||
if n.Tag != nil {
|
||||
for _, stmt := range n.Body.List {
|
||||
clause, ok := stmt.(*ast.CaseClause)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, expr := range clause.List {
|
||||
a.compare(n.Tag, expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.CallExpr:
|
||||
a.call(n)
|
||||
case *ast.CompositeLit:
|
||||
a.composite(n)
|
||||
}
|
||||
ast.Inspect(n, func(child ast.Node) bool {
|
||||
if child == n {
|
||||
return true
|
||||
}
|
||||
if child != nil {
|
||||
a.inspect(child, fn)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) compare(left, right ast.Expr) {
|
||||
leftInterface := isChannelInterface(a.info.TypeOf(left))
|
||||
rightInterface := isChannelInterface(a.info.TypeOf(right))
|
||||
if a.value(left) != nil && (a.value(right) != nil || rightInterface) ||
|
||||
a.value(right) != nil && leftInterface {
|
||||
a.require(left, channelBoth)
|
||||
a.require(right, channelBoth)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) assign(lhs, rhs []ast.Expr) {
|
||||
for i, source := range a.values(rhs) {
|
||||
if i >= len(lhs) {
|
||||
break
|
||||
}
|
||||
if id, ok := lhs[i].(*ast.Ident); ok && id.Name == "_" {
|
||||
continue
|
||||
}
|
||||
a.bindValue(source, a.destination(lhs[i]), a.info.TypeOf(lhs[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) bindResults(exprs []ast.Expr, target *channelFunction) {
|
||||
for i, source := range a.values(exprs) {
|
||||
if i >= len(target.results) {
|
||||
break
|
||||
}
|
||||
a.bindValue(source, target.results[i], target.resultTypes[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) call(call *ast.CallExpr) {
|
||||
a.markDirect(call.Fun)
|
||||
if obj, ok := a.object(call.Fun).(*types.Builtin); ok {
|
||||
switch obj.Name() {
|
||||
case "close":
|
||||
if len(call.Args) == 1 {
|
||||
a.require(call.Args[0], channelSend)
|
||||
}
|
||||
case "append":
|
||||
values := a.values(call.Args)
|
||||
if len(values) > 1 && !call.Ellipsis.IsValid() && values[0].typ != nil {
|
||||
if slice, ok := values[0].typ.Underlying().(*types.Slice); ok {
|
||||
for _, source := range values[1:] {
|
||||
a.bindValue(source, nil, slice.Elem())
|
||||
}
|
||||
}
|
||||
}
|
||||
case "len", "cap", "make":
|
||||
default:
|
||||
for _, source := range a.values(call.Args) {
|
||||
a.bindValue(source, nil, nil)
|
||||
a.escapeType(source.typ, make(map[types.Type]bool))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if a.info.Types[call.Fun].IsType() {
|
||||
for _, arg := range call.Args {
|
||||
a.escapeType(a.info.TypeOf(arg), make(map[types.Type]bool))
|
||||
a.bind(arg, nil, a.info.TypeOf(call))
|
||||
}
|
||||
return
|
||||
}
|
||||
callType := a.info.TypeOf(call.Fun)
|
||||
if callType == nil {
|
||||
return
|
||||
}
|
||||
sig, _ := callType.Underlying().(*types.Signature)
|
||||
if sig == nil {
|
||||
return
|
||||
}
|
||||
callee := a.function(call.Fun)
|
||||
if sel, ok := ast.Unparen(call.Fun).(*ast.SelectorExpr); ok {
|
||||
if selection := a.info.Selections[sel]; selection != nil && selection.Kind() == types.MethodExpr {
|
||||
// Method expressions prepend the receiver; keep their signatures fixed.
|
||||
a.freezeFunction(callee)
|
||||
callee = nil
|
||||
}
|
||||
}
|
||||
for i, source := range a.values(call.Args) {
|
||||
index := i
|
||||
if sig.Variadic() && index >= sig.Params().Len()-1 {
|
||||
index = sig.Params().Len() - 1
|
||||
}
|
||||
if index < 0 || index >= sig.Params().Len() {
|
||||
a.bindValue(source, nil, nil)
|
||||
continue
|
||||
}
|
||||
var dest *channelSlot
|
||||
if callee != nil && index < len(callee.params) {
|
||||
dest = callee.params[index]
|
||||
}
|
||||
typ := sig.Params().At(index).Type()
|
||||
if sig.Variadic() && !call.Ellipsis.IsValid() && index == sig.Params().Len()-1 {
|
||||
if slice, ok := typ.Underlying().(*types.Slice); ok {
|
||||
typ = slice.Elem()
|
||||
}
|
||||
}
|
||||
if callee == nil {
|
||||
a.escapeType(source.typ, make(map[types.Type]bool))
|
||||
}
|
||||
a.bindValue(source, dest, typ)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) freezeFunction(fn *channelFunction) {
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
for _, slots := range [][]*channelSlot{fn.params, fn.results} {
|
||||
for _, slot := range slots {
|
||||
if slot != nil {
|
||||
slot.needs |= channelBoth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) escapeType(typ types.Type, seen map[types.Type]bool) {
|
||||
if typ == nil || seen[typ] {
|
||||
return
|
||||
}
|
||||
seen[typ] = true
|
||||
switch typ := typ.Underlying().(type) {
|
||||
case *types.Pointer:
|
||||
a.escapeType(typ.Elem(), seen)
|
||||
case *types.Struct:
|
||||
for field := range typ.Fields() {
|
||||
if slot := a.variable(field, false); slot != nil {
|
||||
slot.needs |= channelBoth
|
||||
}
|
||||
a.escapeType(field.Type(), seen)
|
||||
}
|
||||
case *types.Array:
|
||||
a.escapeType(typ.Elem(), seen)
|
||||
case *types.Chan:
|
||||
a.escapeType(typ.Elem(), seen)
|
||||
case *types.Slice:
|
||||
a.escapeType(typ.Elem(), seen)
|
||||
case *types.Map:
|
||||
a.escapeType(typ.Key(), seen)
|
||||
a.escapeType(typ.Elem(), seen)
|
||||
case *types.Signature:
|
||||
for v := range typ.Params().Variables() {
|
||||
a.escapeType(v.Type(), seen)
|
||||
}
|
||||
for v := range typ.Results().Variables() {
|
||||
a.escapeType(v.Type(), seen)
|
||||
}
|
||||
case *types.Interface:
|
||||
for method := range typ.Complete().Methods() {
|
||||
a.escapeType(method.Type(), seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *channelAnalysis) composite(lit *ast.CompositeLit) {
|
||||
typ := a.info.TypeOf(lit)
|
||||
if typ == nil {
|
||||
return
|
||||
}
|
||||
st, _ := typ.Underlying().(*types.Struct)
|
||||
for i, elt := range lit.Elts {
|
||||
expr := elt
|
||||
var field types.Object
|
||||
if kv, ok := elt.(*ast.KeyValueExpr); ok {
|
||||
expr = kv.Value
|
||||
if mapping, ok := typ.Underlying().(*types.Map); ok {
|
||||
a.bind(kv.Key, nil, mapping.Key())
|
||||
a.bind(kv.Value, nil, mapping.Elem())
|
||||
continue
|
||||
}
|
||||
field = a.object(kv.Key)
|
||||
} else if st != nil && i < st.NumFields() {
|
||||
field = st.Field(i)
|
||||
}
|
||||
if field != nil {
|
||||
a.bind(expr, a.variable(field, false), field.Type())
|
||||
} else {
|
||||
switch container := typ.Underlying().(type) {
|
||||
case *types.Array:
|
||||
a.bind(expr, nil, container.Elem())
|
||||
case *types.Slice:
|
||||
a.bind(expr, nil, container.Elem())
|
||||
default:
|
||||
a.require(expr, channelBoth)
|
||||
a.escapeType(a.info.TypeOf(expr), make(map[types.Type]bool))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dgageot/rubocop-go/coptest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChannelDirection(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name, src string
|
||||
want []string
|
||||
}{
|
||||
{"send", `func f(c chan int) { c <- 1 }`, []string{"parameter c:send-only"}},
|
||||
{"close", `func f(c chan int) { close(c) }`, []string{"parameter c:send-only"}},
|
||||
{"receive", `func f(c chan int) { <-c }`, []string{"parameter c:receive-only"}},
|
||||
{"range", `func f(c chan int) { for range c {} }`, []string{"parameter c:receive-only"}},
|
||||
{"both", `func f(c chan int) { close(c); <-c }`, nil},
|
||||
{"unused", `func f(c chan int) { _ = len(c); _ = cap(c); _ = c == nil }`, nil},
|
||||
{"alias", `func f(c chan int) { x := c; <-x }`, []string{"parameter c:receive-only"}},
|
||||
{"helper", `func f(c chan int) { g(c) }; func g(d chan int) { close(d) }`, []string{"parameter c:send-only", "parameter d:send-only"}},
|
||||
{"cycle", `func f(c chan int) { g(c) }; func g(d chan int) { <-d; f(d) }`, []string{"parameter c:receive-only", "parameter d:receive-only"}},
|
||||
{"sink", `type sink struct{ c chan int }; func newSink(c chan int) *sink { return &sink{c:c} }; func (s *sink) emit() { s.c <- 1 }`, []string{"parameter c:send-only"}},
|
||||
{"sink interface", `type Sink interface{ emit() }; type sink struct{ c chan int }; func newSink(c chan int) Sink { return &sink{c:c} }; func (s *sink) emit() { s.c <- 1 }`, nil},
|
||||
{"producer", `func f() chan int { c := make(chan int); go func(){ c <- 1; close(c) }(); return c }; func g(){ <-f() }`, []string{"result:receive-only"}},
|
||||
{"passthrough", `func f(c chan int) chan int { return c }; func g(){ <-f(make(chan int)) }`, []string{"parameter c:receive-only", "result:receive-only"}},
|
||||
{"fixed receiver", `func f(c chan int) { close(c); g(c) }; func g(<-chan int) {}`, nil},
|
||||
{"explicit conversion", `func f(c chan int) { <-c; _ = (chan int)(c) }`, nil},
|
||||
{"directional conversion", `func f(c chan int) { _ = (<-chan int)(c) }`, []string{"parameter c:receive-only"}},
|
||||
{"function value", `func f(c chan int) { <-c }; var callback func(chan int) = f`, nil},
|
||||
{"method interface", `type I interface{ Read(chan int) }; type T struct{}; func (T) Read(c chan int) { <-c }; var _ I = T{}`, nil},
|
||||
{"unknown callback", `func f(c chan int, g func(chan int)) { g(c); <-c }`, nil},
|
||||
{"boxed channel", `func f(c chan int) any { <-c; return c }`, nil},
|
||||
{"channel comparison", `func f(a,b chan int) bool { close(a); <-b; return a == b }`, nil},
|
||||
{"address", `func f(c chan int) *chan int { <-c; return &c }`, nil},
|
||||
{"stored interface", `func f(c chan int) { var a any = c; _ = a; <-c }`, nil},
|
||||
{"container", `func f(c chan int) []chan int { <-c; return []chan int{c} }`, nil},
|
||||
{"channel element", `func f(dst chan chan int, c chan int) { dst <- c; <-c }`, []string{"parameter dst:send-only"}},
|
||||
{"named", `type C chan int; func f(c C) { <-c }`, nil},
|
||||
{"type alias", `type C = chan int; func f(c C) { <-c }`, nil},
|
||||
{"closure", `func f(c chan int) { go func(){ <-c }() }`, []string{"parameter c:receive-only"}},
|
||||
{"tuple", `func f() (chan int, int) { return make(chan int), 1 }; func g(){ c,_ := f(); <-c }`, []string{"result:receive-only"}},
|
||||
{"named result", `func f() (c chan int) { c = make(chan int); return }; func g(){ <-f() }`, []string{"result c:receive-only"}},
|
||||
{"generic", `func g[C ~chan int](c C){ <-c }; func f(c chan int){ g(c); <-c }`, nil},
|
||||
{"named struct conversion", `type A struct{c chan int}; type B struct{c chan int}; func f(c chan int){ <-c; b:=B(A{c}); b.c<-1 }`, nil},
|
||||
{"literal assigned", `var callback func(chan int) = func(c chan int){ <-c }`, nil},
|
||||
{"literal passed", `func g(func(chan int)){}; func f(){ g(func(c chan int){ <-c }) }`, nil},
|
||||
{"map store", `func f(c chan int, m map[string]chan int){ <-c; m["x"] = c }`, nil},
|
||||
{"pointer store", `func f(c chan int, p *chan int){ <-c; *p = c }`, nil},
|
||||
{"map key", `func f(c chan int){ <-c; _ = map[chan int]int{c:1} }`, nil},
|
||||
{"anonymous interface", `type T struct{}; func (T) Read(c chan int){ <-c }; var _ interface{Read(chan int)} = T{}`, nil},
|
||||
{"local interface", `type T struct{}; func (T) Read(c chan int){ <-c }; func f(){ type I interface{Read(chan int)}; var _ I = T{} }`, nil},
|
||||
{"tuple arguments", `func pair()(chan int,int){ return make(chan int),0 }; func send(c chan int,n int){ c<-n }; func f(){ send(pair()); c,_:=pair(); <-c }`, []string{"parameter c:send-only"}},
|
||||
{"boxed tuple return", `func pair()(chan int,int){ return make(chan int),0 }; func f()(any,int){ return pair() }; func g(){ c,_:=pair(); <-c }`, nil},
|
||||
{"boxed struct", `func f(c chan int) any { <-c; return struct{ C chan int }{c} }`, nil},
|
||||
{"anonymous struct result", `func f(c chan int) struct{ C chan int }{ <-c; return struct{ C chan int }{c} }; func g(){ f(make(chan int)).C<-1 }`, nil},
|
||||
{"method direct", `type T struct{}; func(T) f(c chan int){ <-c }; func g(c chan int){ T{}.f(c) }`, []string{"parameter c:receive-only", "parameter c:receive-only"}},
|
||||
{"method expression", `type T struct{}; func(T) f(c chan int){ <-c }; func g(c chan int){ T.f(T{},c); <-c }`, nil},
|
||||
{"aggregate escape", `type box struct{ c chan int }; func f(c chan int, g func(box)){ g(box{c:c}); <-c }`, nil},
|
||||
{"structural assignment", `type Box struct{ C chan int }; func f(c chan int){ <-c; b:=Box{c}; var out struct{ C chan int }=b; out.C<-1 }`, nil},
|
||||
{"structural pointer assignment", `type Box struct{ C chan int }; func f(c chan int){ <-c; b:=&Box{c}; var out *struct{ C chan int }=(*struct{ C chan int })(b); out.C<-1 }`, nil},
|
||||
{"structural argument", `type Box struct{ C chan int }; func g(b struct{ C chan int }){ b.C<-1 }; func f(c chan int){ <-c; g(Box{c}) }`, nil},
|
||||
{"structural return", `type Box struct{ C chan int }; func f(c chan int) struct{ C chan int }{ <-c; return Box{c} }; func g(){ f(make(chan int)).C<-1 }`, nil},
|
||||
{"anonymous aliases", `type A=struct{ C chan int }; type B=struct{ C chan int }; func f(c chan int){ <-c; a:=A{c}; var b B=a; b.C<-1 }`, nil},
|
||||
{"aggregate container element", `type Box struct{ C chan int }; func f(c chan int){ <-c; out:=[]struct{ C chan int }{Box{c}}; out[0].C<-1 }`, nil},
|
||||
{"aggregate map element", `type Box struct{ C chan int }; func f(c chan int){ <-c; out:=map[int]struct{ C chan int }{0:Box{c}}; out[0].C<-1 }`, nil},
|
||||
{"tuple interface argument", `import "reflect"; type Box struct{ C chan int }; func pair(c chan int)(Box,int){ <-c; return Box{c},0 }; func observe(v any, _ int) reflect.ChanDir { return reflect.TypeOf(v).Field(0).Type.ChanDir() }; func g(c chan int){ observe(pair(c)) }`, nil},
|
||||
{"tuple interface assignment", `type Box struct{ C chan int }; func pair(c chan int)(Box,int){ <-c; return Box{c},0 }; func g(c chan int) any { var v any; v,_=pair(c); return v }`, nil},
|
||||
{"tuple interface return", `type Box struct{ C chan int }; func pair(c chan int)(Box,int){ <-c; return Box{c},0 }; func g(c chan int)(any,int){ return pair(c) }`, nil},
|
||||
{"tuple unknown callback", `type Box struct{ C chan int }; func pair(c chan int)(Box,int){ <-c; return Box{c},0 }; func g(c chan int, cb func(Box,int)){ cb(pair(c)) }`, nil},
|
||||
{"tuple variadic argument", `type Box struct{ C chan int }; func pair(c chan int)(int,Box){ <-c; return 0,Box{c} }; func observe(_ int, vs ...any) any { return vs }; func g(c chan int){ observe(pair(c)) }`, nil},
|
||||
{"tuple variadic only", `type Box struct{ C chan int }; func pair(c chan int)(Box,int){ <-c; return Box{c},0 }; func observe(vs ...any) any { return vs }; func g(c chan int){ observe(pair(c)) }`, nil},
|
||||
{"boxed channel aggregate", `type Box struct{ C chan int }; func f(c chan int) any { <-c; q:=make(chan Box,1); q<-Box{c}; return q }`, nil},
|
||||
{"nonempty interface escape", `type Box struct{ C chan int }; func(Box) Name()string{return "box"}; type I interface{Name()string}; func f(c chan int) I { <-c; return Box{c} }`, nil},
|
||||
{"panic aggregate", `type Box struct{ C chan int }; func f(c chan int){ <-c; panic(Box{c}) }`, nil},
|
||||
{"append interface aggregate", `type Box struct{ C chan int }; func f(c chan int) []any { <-c; return append([]any{},Box{c}) }`, nil},
|
||||
{"append directional element", `func f(c chan int) []<-chan int { return append([]<-chan int{},c) }`, []string{"parameter c:receive-only"}},
|
||||
{"type-only reflection", `import "reflect"; type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; func direction() reflect.ChanDir { return reflect.TypeFor[Box]().Field(0).Type.ChanDir() }`, nil},
|
||||
{"pointer type-only reflection", `import "reflect"; type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; func direction() reflect.ChanDir { return reflect.TypeFor[*Box]().Elem().Field(0).Type.ChanDir() }`, nil},
|
||||
{"nested type-only reflection", `import "reflect"; type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; func direction() reflect.Type { return reflect.TypeFor[map[string][]Box]() }`, nil},
|
||||
{"nonempty interface argument", `type Box struct{ C chan int }; func(Box) Name() string { return "box" }; type I interface{ Name() string }; func keep(v I) I { return v }; func f(c chan int) I { <-c; return keep(Box{c}) }`, nil},
|
||||
{"unknown generic type arguments", `type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; func observe[A,B any](){}; func g(){ observe[int,Box]() }`, nil},
|
||||
{"tuple directional argument", `func pair()(chan int,int){ return make(chan int),0 }; func read(c <-chan int,_ int){ <-c }; func g(){ read(pair()) }`, []string{"result:receive-only"}},
|
||||
{"switch channel comparison", `func f(c chan int,d chan<- int)bool{ <-c; switch c { case d:return true }; return false }`, nil},
|
||||
{"switch function result", `func f(c chan int)chan int{return c}; func g(d chan<- int){ c:=f(make(chan int)); <-c; switch c { case d: } }`, nil},
|
||||
{"interface equality", `func f(c chan int,v any)bool{ <-c; return c==v }`, nil},
|
||||
{"interface switch", `func f(c chan int,v any)bool{ <-c; switch c { case v:return true }; return false }`, nil},
|
||||
{"interface switch tag", `func f(c chan int,v any)bool{ <-c; switch v { case c:return true }; return false }`, nil},
|
||||
{"append tuple", `func pair(c chan int)([]chan int,chan int){ <-c; return nil,c }; func f(c chan int){ xs:=append(pair(c)); xs[0]<-1 }`, nil},
|
||||
{"delete tuple", `type Box struct{ C chan int }; func pair(c chan int)(map[any]int,any){ <-c; return nil,Box{c} }; func f(c chan int){ delete(pair(c)) }`, nil},
|
||||
{"function type reflection", `import "reflect"; type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; func observe() reflect.Type { return reflect.TypeFor[func() Box]() }`, nil},
|
||||
{"interface method type reflection", `import "reflect"; type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; func observe() reflect.Type { return reflect.TypeFor[interface{ Get() Box }]() }`, nil},
|
||||
{"boxed function", `type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; var v any=f`, nil},
|
||||
{"inferred generic constraint", `type Box struct{ C chan int }; func accept[T ~struct{ C chan int }](v T){}; func f(c chan int){ <-c; accept(Box{c}) }`, nil},
|
||||
{"inferred generic reflection", `import "reflect"; type Box struct{ C chan int }; func accept[T any](v T) reflect.Type{return reflect.TypeOf(v)}; func f(c chan int){ <-c; accept(Box{c}) }`, nil},
|
||||
{"inferred generic type reflection", `import "reflect"; type Box struct{ C chan int }; func accept[T any](v T) reflect.Type{return reflect.TypeFor[T]()}; func f(c chan int){ <-c; accept(Box{c}) }`, nil},
|
||||
{"generic function value constraint", `type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; func accept[T ~struct{ C chan int }](){}; var invoke=accept[Box]; func g(){ invoke() }`, nil},
|
||||
{"generic function value reflection", `import "reflect"; type Box struct{ C chan int }; func f(c chan int) Box { <-c; return Box{c} }; var observe=reflect.TypeFor[Box]; func g() reflect.Type { return observe() }`, nil},
|
||||
{"switch nil", `func f(c chan int)bool{ <-c; switch c { case nil:return true }; return false }`, []string{"parameter c:receive-only"}},
|
||||
}
|
||||
files := coptest.ProgramFiles{"go.mod": "module example.test\n\ngo 1.27\n"}
|
||||
for i, tc := range tests {
|
||||
files[fmt.Sprintf("case%d/sample.go", i)] = "package p\n" + tc.src
|
||||
}
|
||||
offenses := coptest.RunProgram(t, ChannelDirection, files)
|
||||
got := make(map[string][]string)
|
||||
for _, offense := range offenses {
|
||||
label, rest, ok := strings.Cut(offense.Message, " can be ")
|
||||
assert.True(t, ok)
|
||||
direction, _, _ := strings.Cut(rest, " ")
|
||||
key := filepath.Base(filepath.Dir(offense.Pos.Filename))
|
||||
got[key] = append(got[key], label+":"+direction)
|
||||
}
|
||||
for i, tc := range tests {
|
||||
assert.ElementsMatch(t, tc.want, got[fmt.Sprintf("case%d", i)], tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDirectionPackagesAndTests(t *testing.T) {
|
||||
t.Parallel()
|
||||
offenses := coptest.RunProgram(t, ChannelDirection, coptest.ProgramFiles{
|
||||
"go.mod": "module example.test\n\ngo 1.27\n",
|
||||
"sink/sink.go": `package sink
|
||||
func Send(c chan int) { c <- 1 }
|
||||
func Read(c chan int) { <-c }
|
||||
func Both(c chan int) { <-c }
|
||||
`,
|
||||
"sink/sink_test.go": `package sink
|
||||
import "testing"
|
||||
func TestBoth(t *testing.T) { c := make(chan int,1); c <- 1; Both(c) }
|
||||
func send(c chan int) { c <- 1; Both(c) }
|
||||
func testHelper(c chan int) { close(c) }
|
||||
var _ func(chan int) = Both
|
||||
`,
|
||||
"consumer/consumer.go": `package consumer
|
||||
import "example.test/sink"
|
||||
func Forward(c chan int) { sink.Send(c) }
|
||||
`,
|
||||
"sink/external_test.go": `package sink_test
|
||||
import "example.test/sink"
|
||||
func helper(c chan int) { sink.Read(c) }
|
||||
`,
|
||||
"testonly/helper_test.go": `package testonly
|
||||
func helper(c chan int) { <-c }
|
||||
`,
|
||||
})
|
||||
var got []string
|
||||
for _, offense := range offenses {
|
||||
got = append(got, filepath.Base(offense.Pos.Filename)+":"+strconv.Itoa(offense.Pos.Line))
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"sink.go:2", "sink.go:3", "consumer.go:3", "sink_test.go:5", "external_test.go:3", "helper_test.go:2"}, got)
|
||||
}
|
||||
|
||||
func TestChannelDirectionScopes(t *testing.T) {
|
||||
t.Parallel()
|
||||
const source = "package p\nfunc Send(c chan int) { c <- 1 }\n"
|
||||
offenses := coptest.RunProgram(t, ChannelDirection, coptest.ProgramFiles{
|
||||
"pkg/config/v0/sample.go": source,
|
||||
"pkg/config/latest/sample.go": source,
|
||||
"generated/sample.go": "// Code generated by test; DO NOT EDIT.\n" + source,
|
||||
"caller/sample.go": `package p
|
||||
import old "example.test/pkg/config/v0"
|
||||
func f(c chan int) { old.Send(c) }
|
||||
`,
|
||||
})
|
||||
assert.Len(t, offenses, 1)
|
||||
if len(offenses) == 1 {
|
||||
assert.Contains(t, filepath.ToSlash(offenses[0].Pos.Filename), "/latest/")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDirectionBridge(t *testing.T) {
|
||||
t.Parallel()
|
||||
offenses := coptest.RunProgram(t, ChannelDirection, coptest.ProgramFiles{
|
||||
"bridge.go": `package p
|
||||
type bridge struct { ch chan int }
|
||||
func (b *bridge) swap(ch chan int) chan int { prev := b.ch; b.ch = ch; return prev }
|
||||
func (b *bridge) restore(current, previous chan int) { b.ch = previous; close(current) }
|
||||
func (b *bridge) send() { b.ch <- 1 }
|
||||
func (b *bridge) run(events chan int) { prev := b.swap(events); b.restore(events, prev) }
|
||||
`,
|
||||
})
|
||||
assert.Len(t, offenses, 5)
|
||||
for _, offense := range offenses {
|
||||
assert.Contains(t, offense.Message, "send-only")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDirectionInvalidTests(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Empty(t, coptest.RunProgram(t, ChannelDirection, coptest.ProgramFiles{
|
||||
"sample.go": "package p\nfunc f(c chan int) { <-c }",
|
||||
"sample_test.go": "package p\nfunc broken() { close() }\nfunc bodyless(c chan int)",
|
||||
}))
|
||||
}
|
||||
@@ -76,6 +76,7 @@ var programCops = []prog.Cop{
|
||||
rubocops.NewLintCutSuffix(cop.WithScope(outsideFrozenConfig)),
|
||||
rubocops.NewLintFieldsSeqLookup(),
|
||||
SlicesConcat,
|
||||
ChannelDirection,
|
||||
SessionStateAccessors,
|
||||
rubocops.NewLintStreamCloseSafety(),
|
||||
ExclusiveStreamLease,
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
func TestSharedCopRegistrations(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Len(t, cops, 31)
|
||||
require.Len(t, programCops, 28)
|
||||
require.Len(t, programCops, 29)
|
||||
counts := make(map[string]int)
|
||||
for _, c := range cops {
|
||||
counts[c.Name()]++
|
||||
@@ -31,7 +31,7 @@ func TestSharedCopRegistrations(t *testing.T) {
|
||||
for _, c := range programCops {
|
||||
counts[c.Name()]++
|
||||
}
|
||||
assert.Len(t, counts, 59)
|
||||
assert.Len(t, counts, 60)
|
||||
for _, name := range []string{"Lint/ContextFirstParameter", "Lint/NoContextField"} {
|
||||
assert.NotContains(t, counts, name)
|
||||
}
|
||||
|
||||
+1
-1
@@ -591,7 +591,7 @@ func (a *App) LookupCommand(ctx context.Context, userInput string) (types.Comman
|
||||
}
|
||||
|
||||
// EmitStartupInfo emits initial agent, team, and toolset information to the provided channel
|
||||
func (a *App) EmitStartupInfo(ctx context.Context, events chan runtime.Event) {
|
||||
func (a *App) EmitStartupInfo(ctx context.Context, events chan<- runtime.Event) {
|
||||
a.runtime.EmitStartupInfo(ctx, a.session, runtime.NewChannelSink(events))
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ func resolveKeys() {
|
||||
// model is the top-level bubbletea model of the board.
|
||||
type model struct {
|
||||
app *board.App
|
||||
refresh chan struct{}
|
||||
refresh <-chan struct{}
|
||||
|
||||
width, height int
|
||||
|
||||
@@ -261,7 +261,7 @@ type model struct {
|
||||
dragCol int
|
||||
}
|
||||
|
||||
func newModel(app *board.App, refresh chan struct{}) *model {
|
||||
func newModel(app *board.App, refresh <-chan struct{}) *model {
|
||||
resolveKeys()
|
||||
m := &model{
|
||||
app: app,
|
||||
|
||||
@@ -107,7 +107,7 @@ func (p *runtimePool) takeIdle(agent string) runtimeRunner {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *runtimePool) channelFor(agent string) chan runtimeRunner {
|
||||
func (p *runtimePool) channelFor(agent string) chan<- runtimeRunner {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
ch, ok := p.idle[agent]
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// cancelled, then floods more events than any buffer holds before closing.
|
||||
// done is closed only once every event has been consumed, so Run can only
|
||||
// observe it closed if it cancelled the producer and drained the stream.
|
||||
func stallingRunStream(triggers []runtime.Event, done chan struct{}) func(context.Context, *session.Session) <-chan runtime.Event {
|
||||
func stallingRunStream(triggers []runtime.Event, done chan<- struct{}) func(context.Context, *session.Session) <-chan runtime.Event {
|
||||
return func(ctx context.Context, _ *session.Session) <-chan runtime.Event {
|
||||
ch := make(chan runtime.Event)
|
||||
go func() {
|
||||
|
||||
@@ -114,10 +114,10 @@ func TestManager_Query_UsageAggregation(t *testing.T) {
|
||||
// until release is closed.
|
||||
type blockingStrategy struct {
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
release <-chan struct{}
|
||||
}
|
||||
|
||||
func newBlockingStrategy(release chan struct{}) *blockingStrategy {
|
||||
func newBlockingStrategy(release <-chan struct{}) *blockingStrategy {
|
||||
return &blockingStrategy{started: make(chan struct{}), release: release}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ func (b *elicitationBridge) send(ctx context.Context, ev Event) (err error) {
|
||||
// process with a send-on-closed-channel panic; #3584 bounded the wait (send
|
||||
// used to have no ctx at all and could block indefinitely) and moved the
|
||||
// caller onto a detached goroutine so this can never stall the request path.
|
||||
func (b *elicitationBridge) restoreAndClose(current, previous chan Event) {
|
||||
func (b *elicitationBridge) restoreAndClose(current chan<- Event, previous chan Event) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.ch = previous
|
||||
|
||||
@@ -26,14 +26,14 @@ type EventSink interface {
|
||||
// is dropped, since a closed channel signals that the consumer has
|
||||
// gone away.
|
||||
type channelSink struct {
|
||||
ch chan Event
|
||||
ch chan<- Event
|
||||
}
|
||||
|
||||
// NewChannelSink returns an [EventSink] that writes to ch. This is the
|
||||
// standard bridge between the runtime's EventSink-based internals and
|
||||
// external callers that create raw event channels (e.g. app.go,
|
||||
// tests).
|
||||
func NewChannelSink(ch chan Event) EventSink {
|
||||
func NewChannelSink(ch chan<- Event) EventSink {
|
||||
return &channelSink{ch: ch}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (s *channelSink) Emit(e Event) {
|
||||
// regular runtime code should use the blocking [channelSink] so
|
||||
// back-pressure is preserved.
|
||||
type nonBlockingChannelSink struct {
|
||||
ch chan Event
|
||||
ch chan<- Event
|
||||
}
|
||||
|
||||
// nonBlocking returns a non-blocking sink for sink. If sink wraps a
|
||||
@@ -80,7 +80,7 @@ func (s nonBlockingChannelSink) Emit(e Event) {
|
||||
// should use the unbounded blocking [channelSink] instead, so ordinary
|
||||
// back-pressure is preserved.
|
||||
type boundedChannelSink struct {
|
||||
ch chan Event
|
||||
ch chan<- Event
|
||||
wait time.Duration
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -215,7 +215,7 @@ func (r *LocalRuntime) emitHookDrivenShutdown(
|
||||
// is bounded rather than unbounded). Consumers must still rely on the channel
|
||||
// close, not on receiving StreamStopped, as the one guaranteed terminal
|
||||
// signal.
|
||||
func (r *LocalRuntime) finalizeEventChannel(ctx context.Context, sess *session.Session, reason string, finishReason chat.FinishReason, prevElicitationCh, events chan Event) {
|
||||
func (r *LocalRuntime) finalizeEventChannel(ctx context.Context, sess *session.Session, reason string, finishReason chat.FinishReason, prevElicitationCh chan Event, events chan<- Event) {
|
||||
a := r.resolveSessionAgent(sess)
|
||||
|
||||
if ctx.Err() != nil && reason == "" {
|
||||
|
||||
@@ -538,7 +538,7 @@ func awaitEvent[T Event](t *testing.T, ch <-chan Event, what string) T {
|
||||
}
|
||||
}
|
||||
|
||||
func requirePlanChanged(t *testing.T, events chan Event) *PlanChangedEvent {
|
||||
func requirePlanChanged(t *testing.T, events <-chan Event) *PlanChangedEvent {
|
||||
t.Helper()
|
||||
select {
|
||||
case ev := <-events:
|
||||
@@ -551,7 +551,7 @@ func requirePlanChanged(t *testing.T, events chan Event) *PlanChangedEvent {
|
||||
}
|
||||
}
|
||||
|
||||
func requireNoEvent(t *testing.T, events chan Event) {
|
||||
func requireNoEvent(t *testing.T, events <-chan Event) {
|
||||
t.Helper()
|
||||
select {
|
||||
case ev := <-events:
|
||||
|
||||
@@ -52,7 +52,7 @@ func recordingTool(name string, executed *bool) []tools.Tool {
|
||||
// further events are produced); used after processToolCalls returns.
|
||||
// Distinct from drainEvents in loop_steps_test.go, which is
|
||||
// non-blocking and intended for still-open channels.
|
||||
func collectClosedEvents(ch chan Event) []Event {
|
||||
func collectClosedEvents(ch <-chan Event) []Event {
|
||||
var out []Event
|
||||
for ev := range ch {
|
||||
out = append(out, ev)
|
||||
|
||||
@@ -1047,7 +1047,7 @@ func TestSessionWithoutUserMessage(t *testing.T) {
|
||||
|
||||
// --- Tool setup failure handling tests ---
|
||||
|
||||
func collectEvents(ch chan Event) []Event {
|
||||
func collectEvents(ch <-chan Event) []Event {
|
||||
n := len(ch)
|
||||
evs := make([]Event, 0, n)
|
||||
for range n {
|
||||
|
||||
@@ -599,7 +599,7 @@ func TestHandleStream_ContextCancellation(t *testing.T) {
|
||||
|
||||
// agentChoiceText drains every buffered event and concatenates the
|
||||
// AgentChoice content, i.e. exactly what a live consumer (TUI/API) rendered.
|
||||
func agentChoiceText(ch chan Event) string {
|
||||
func agentChoiceText(ch <-chan Event) string {
|
||||
var b strings.Builder
|
||||
for {
|
||||
select {
|
||||
|
||||
@@ -48,7 +48,7 @@ func newRAGAgent(name string, toolsets ...tools.ToolSet) *agent.Agent {
|
||||
)
|
||||
}
|
||||
|
||||
func requireRAGProgress(t *testing.T, events chan Event) *RAGIndexingProgressEvent {
|
||||
func requireRAGProgress(t *testing.T, events <-chan Event) *RAGIndexingProgressEvent {
|
||||
t.Helper()
|
||||
select {
|
||||
case ev := <-events:
|
||||
|
||||
@@ -118,7 +118,7 @@ func (s *Supervisor) SpawnSession(ctx context.Context, workingDir string) (strin
|
||||
|
||||
// subscribeWithRouting subscribes to app events and wraps them with session ID.
|
||||
// Registration precedes startup; terminal delivery waits for the program.
|
||||
func (s *Supervisor) subscribeWithRouting(ctx context.Context, a *app.App, sessionID string, ready chan struct{}) {
|
||||
func (s *Supervisor) subscribeWithRouting(ctx context.Context, a *app.App, sessionID string, ready chan<- struct{}) {
|
||||
prepare := func(msg tea.Msg, generation uint64) tea.Msg {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
Reference in new issue
Block a user