Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions cmd/internal/compile/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,63 @@ func TestRunCmdBuildsAndReportsErrors(t *testing.T) {
if code != 1 || !strings.Contains(stderr, "go:uintptrkeepalive requires go:nosplit") {
t.Fatalf("-std compile exit code = %d, stderr=%q; want code 1 and pragma diagnostic", code, stderr)
}

invalidNoescape := dir + "/invalid_noescape.go"
if err := os.WriteFile(invalidNoescape, []byte(`package compilecase
//go:unknown
func External()

//go:noescape
// compiler directives remain pending across ordinary comments and blank lines.

func HasBody() {}
`), 0o644); err != nil {
t.Fatal(err)
}
_, stderr, code = runCompileCommand(t, []string{"-C", "-e", "-std", invalidNoescape})
if code != 1 ||
!strings.Contains(stderr, "//go:unknown is not allowed in the standard library") ||
!strings.Contains(stderr, "can only use //go:noescape with external func implementations") {
t.Fatalf("-std compile exit code = %d, stderr=%q; want both standard-library pragma diagnostics", code, stderr)
}

for _, invalid := range []struct {
name string
source string
want string
}{
{
name: "type error",
source: `package compilecase
//go:unknown
func External()

//go:noescape
func HasBody() { _ = missing }
`,
want: "undefined: missing",
},
{
name: "parse error",
source: `package compilecase
var x = )

//go:noescape
func HasBody() {}
`,
want: "syntax error",
},
} {
invalidPath := dir + "/invalid_noescape_" + strings.ReplaceAll(invalid.name, " ", "_") + ".go"
if err := os.WriteFile(invalidPath, []byte(invalid.source), 0o644); err != nil {
t.Fatal(err)
}
_, stderr, code = runCompileCommand(t, []string{"-C", "-e", "-std", invalidPath})
if code != 1 || !strings.Contains(stderr, invalid.want) ||
strings.Contains(stderr, "can only use //go:noescape with external func implementations") {
t.Fatalf("%s exit code = %d, stderr=%q; want code 1, %q, and no noescape-body diagnostic", invalid.name, code, stderr, invalid.want)
}
}
}

func runCompileCommand(t *testing.T, args []string) (stdout, stderr string, exitCode int) {
Expand Down
37 changes: 30 additions & 7 deletions internal/packages/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,17 +481,40 @@ func loadPackageEx(dedup Deduper, ld *loader, lpkg *loaderPackage) {
appendError(typErr)
}

// Record accumulated errors.
illTyped := len(lpkg.Errors) > 0
if !illTyped {
for _, imp := range lpkg.Imports {
if imp.IllTyped {
illTyped = true
hasIllTypedImport := false
for _, imp := range lpkg.Imports {
if imp.IllTyped {
hasIllTypedImport = true
break
}
}

// cmd/compile reaches compiler-directive body checks only after parsing and
// type-checking the complete package and its imports. Do not gate on
// ListError: an earlier compiler-directive diagnostic may itself be why go
// list stopped early. When function bodies are deliberately ignored, their
// type-check phase is incomplete, so writer-only checks cannot be safely
// synthesized.
hasSourceErrors := tc.IgnoreFuncBodies || len(errs) != 0 || typErr != nil ||
Comment thread
cpunion marked this conversation as resolved.
len(lpkg.TypeErrors) != 0 || hasIllTypedImport
if !hasSourceErrors {
for _, err := range lpkg.Errors {
if err.Kind == packages.ParseError || err.Kind == packages.TypeError {
hasSourceErrors = true
break
}
}
}
lpkg.IllTyped = illTyped
if !hasSourceErrors {
for _, err := range validateCompilerDirectives(ld.Fset, lpkg.Syntax) {
if !packageHasCompilerDiagnostic(lpkg.Errors, err) {
appendError(err)
}
}
}

// Record accumulated errors.
lpkg.IllTyped = len(lpkg.Errors) > 0 || hasIllTypedImport
}

func packageGoVersion(ld *loader, lpkg *loaderPackage) string {
Expand Down
135 changes: 135 additions & 0 deletions internal/packages/pragma.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package packages

import (
"go/ast"
"go/token"
"go/types"
"strings"
)

const noescapeBodyDiagnostic = "can only use //go:noescape with external func implementations"

// validateCompilerDirectives applies cmd/compile's //go:noescape body check,
// which is outside go/types. The go command may report this check while loading
// export data, but it can stop before reaching it when an earlier compiler error
// is present, so the source frontend must validate it independently.
func validateCompilerDirectives(fset *token.FileSet, files []*ast.File) []types.Error {
if fset == nil {
return nil
}
var errs []types.Error
for _, file := range files {
if file == nil {
continue
}
previousEnd := file.Name.End()
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
directiveEnd := decl.Pos()
var declarationToken token.Pos
if ok {
declarationToken = fn.Pos()
directiveEnd = compilerFunctionPos(fn)
}
hasNoescape := hasCompilerDirectiveBetween(
fset, file.Comments, previousEnd, directiveEnd, declarationToken, "go:noescape",
)
previousEnd = decl.End()
if !ok || fn.Body == nil || !hasNoescape {
continue
}
errs = append(errs, types.Error{
Fset: fset,
Pos: directiveEnd,
Msg: noescapeBodyDiagnostic,
})
}
}
return errs
}

func compilerFunctionPos(fn *ast.FuncDecl) token.Pos {
if fn.Recv != nil && fn.Recv.Opening.IsValid() {
return fn.Recv.Opening
}
if fn.Name != nil {
return fn.Name.Pos()
}
return fn.Pos()
}

// hasCompilerDirectiveBetween reports whether a standalone directive occurs
// between two declarations. cmd/compile keeps directives pending across blank
// lines and ordinary comments, then consumes them at the next declaration. A
// function directive may also occur between the func keyword and its name (or
// receiver).
func hasCompilerDirectiveBetween(
fset *token.FileSet, groups []*ast.CommentGroup, start, end, declarationToken token.Pos, directive string,
) bool {
previous := start
for _, group := range groups {
if group == nil || group.End() <= start || group.Pos() >= end {
continue
}
for _, comment := range group.List {
if comment == nil || comment.End() <= start || comment.Pos() >= end {
continue
}
if declarationToken > previous && declarationToken < comment.Pos() {
previous = declarationToken
}
standalone := physicalLine(fset, previous) != physicalLine(fset, comment.Pos())
previous = comment.End()
if !standalone || !strings.HasPrefix(comment.Text, "//") {
continue
}
text := strings.TrimPrefix(comment.Text, "//")
if text == directive {
return true
}
if strings.HasPrefix(text, directive) && len(text) > len(directive) {
next := text[len(directive)]
if next == ' ' {
return true
}
}
}
}
return false
}

func physicalLine(fset *token.FileSet, pos token.Pos) int {
return fset.PositionFor(pos, false).Line
}

func packageHasCompilerDiagnostic(errs []Error, want types.Error) bool {
position := want.Fset.Position(want.Pos)
for _, err := range errs {
if err.Msg == want.Msg && sameDiagnosticLine(err.Pos, position) {
return true
}
for _, line := range strings.Split(err.Msg, "\n") {
pos, msg, ok := strings.Cut(line, ": ")
if ok && msg == want.Msg && sameDiagnosticLine(pos, position) {
return true
}
}
}
return false
}
Loading
Loading