cmd/compile: disallow nointerface method satisfying type constraint

Type constraint satisfaction is interface satisfaction. If a type
does not satisfy a regular interface, it should also not satisfy
the same interface as a type constraint. Otherwise, if the type
satisfies the type constraint, one can use that to construct a
(dynamic) interface value with a type that doesn't actually
satisfy the interface. The go:nointerface directive tells the
compiler that the method does not satisfy an interface. Therefore
it should also not satisfy a type constraint.

Fixes #74626.

Change-Id: I7c64c76044a665755e4e74035085daff42447f9e
Reviewed-on: https://go-review.googlesource.com/c/go/+/772620
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Griesemer <gri@google.com>
This commit is contained in:
Cherry Mui 2026-04-30 13:01:28 -04:00 committed by Robert Griesemer
parent 063f8b07c1
commit 8ddf0031cf
8 changed files with 60 additions and 4 deletions

View file

@ -175,6 +175,10 @@ type pragmas struct {
WasmExport *WasmExport
}
func (p *pragmas) Nointerface() bool {
return p.Flag&ir.Nointerface != 0
}
// WasmImport stores metadata associated with the //go:wasmimport pragma
type WasmImport struct {
Pos syntax.Pos

View file

@ -684,6 +684,12 @@ func (check *Checker) funcDecl(obj *Func, decl *declInfo) {
fdecl := decl.fdecl
check.funcType(sig, fdecl.Recv, fdecl.TParamList, fdecl.Type)
if fdecl.Pragma != nil {
if p, ok := fdecl.Pragma.(interface{ Nointerface() bool }); ok && p.Nointerface() {
obj.nointerface = true
}
}
// Set the scope's extent to the complete "func (...) { ... }"
// so that Scope.Innermost works correctly.
sig.scope.pos = fdecl.Pos()

View file

@ -390,6 +390,7 @@ func (check *Checker) missingMethod(V, T Type, static bool, equivalent func(x, y
ambigSel
ptrRecv
field
nointerface
)
state := ok
@ -453,6 +454,11 @@ func (check *Checker) missingMethod(V, T Type, static bool, equivalent func(x, y
check.objDecl(f)
}
if f.nointerface {
state = nointerface
break
}
if !equivalent(f.typ, m.typ) {
state = wrongSig
break
@ -512,6 +518,8 @@ func (check *Checker) missingMethod(V, T Type, static bool, equivalent func(x, y
*cause = check.sprintf("(method %s has pointer receiver)", m.Name())
case field:
*cause = check.sprintf("(%s.%s is a field, not a method)", V, m.Name())
case nointerface:
*cause = check.sprintf("(%s method is marked 'nointerface')", m.Name())
default:
panic("unreachable")
}

View file

@ -396,8 +396,9 @@ func (*Var) isDependency() {} // a variable may be a dependency of an initializa
// An abstract method may belong to many interfaces due to embedding.
type Func struct {
object
hasPtrRecv_ bool // only valid for methods that don't have a type yet; use hasPtrRecv() to read
origin *Func // if non-nil, the Func from which this one was instantiated
hasPtrRecv_ bool // only valid for methods that don't have a type yet; use hasPtrRecv() to read
nointerface bool
}
// NewFunc returns a new function with the given signature, representing
@ -412,7 +413,7 @@ func NewFunc(pos syntax.Pos, pkg *Package, name string, sig *Signature) *Func {
// as this would violate object.{Type,color} invariants.
// TODO(adonovan): propose to disallow NewFunc with nil *Signature.
}
return &Func{object{nil, pos, pkg, name, typ, 0, nopos}, false, nil}
return &Func{object{nil, pos, pkg, name, typ, 0, nopos}, nil, false, false}
}
// Signature returns the signature (type) of the function or method.

View file

@ -771,6 +771,9 @@ func (check *Checker) funcDecl(obj *Func, decl *declInfo) {
fdecl := decl.fdecl
check.funcType(sig, fdecl.Recv, fdecl.Type)
// types2 handles go:nointerface pragma here by setting obj.nointerface.
// go/types currently doesn't handle pragmas.
// Set the scope's extent to the complete "func (...) { ... }"
// so that Scope.Innermost works correctly.
sig.scope.pos = fdecl.Pos()

View file

@ -393,6 +393,7 @@ func (check *Checker) missingMethod(V, T Type, static bool, equivalent func(x, y
ambigSel
ptrRecv
field
nointerface
)
state := ok
@ -456,6 +457,11 @@ func (check *Checker) missingMethod(V, T Type, static bool, equivalent func(x, y
check.objDecl(f)
}
if f.nointerface {
state = nointerface
break
}
if !equivalent(f.typ, m.typ) {
state = wrongSig
break
@ -515,6 +521,8 @@ func (check *Checker) missingMethod(V, T Type, static bool, equivalent func(x, y
*cause = check.sprintf("(method %s has pointer receiver)", m.Name())
case field:
*cause = check.sprintf("(%s.%s is a field, not a method)", V, m.Name())
case nointerface:
*cause = check.sprintf("(%s method is marked 'nointerface')", m.Name())
default:
panic("unreachable")
}

View file

@ -399,8 +399,9 @@ func (*Var) isDependency() {} // a variable may be a dependency of an initializa
// An abstract method may belong to many interfaces due to embedding.
type Func struct {
object
hasPtrRecv_ bool // only valid for methods that don't have a type yet; use hasPtrRecv() to read
origin *Func // if non-nil, the Func from which this one was instantiated
hasPtrRecv_ bool // only valid for methods that don't have a type yet; use hasPtrRecv() to read
nointerface bool
}
// NewFunc returns a new function with the given signature, representing
@ -415,7 +416,7 @@ func NewFunc(pos token.Pos, pkg *Package, name string, sig *Signature) *Func {
// as this would violate object.{Type,color} invariants.
// TODO(adonovan): propose to disallow NewFunc with nil *Signature.
}
return &Func{object{nil, pos, pkg, name, typ, 0, nopos}, false, nil}
return &Func{object{nil, pos, pkg, name, typ, 0, nopos}, nil, false, false}
}
// Signature returns the signature (type) of the function or method.

View file

@ -0,0 +1,25 @@
// errorcheck -goexperiment fieldtrack
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
type Fooer interface {
Foo() string
}
type FooImpl struct{}
//go:nointerface
func (FooImpl) Foo() string { return "foo" }
func toInterface[T Fooer](fooer T) Fooer {
return fooer
}
func main() {
var iface Fooer = toInterface(FooImpl{}) // ERROR "does not satisfy Fooer"
iface.Foo()
}