fix(11.1-07): require built, run src= code; case-sensitive go doc; parse command forms
- .go src= targets must be in the default build and reached from a Test or an Example with output - go doc -c makes the identifier fallback case-sensitive - commandWord parses env prefixes, flags, go run and bin/ forms
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Ref is a src= reference in a code fence info string: a
|
||||
@@ -106,11 +109,8 @@ func Extract(root string, ref Ref) (string, error) {
|
||||
if err := checkRootModule(rootReal, filepath.Dir(real)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !isTest {
|
||||
tests, _ := filepath.Glob(filepath.Join(filepath.Dir(real), "*_test.go"))
|
||||
if len(tests) == 0 {
|
||||
return "", snippetError("package has no _test.go files, so go test does not cover it")
|
||||
}
|
||||
if err := checkBuiltGo(ref.Path, real, isTest); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if ref.Fragment == "" {
|
||||
@@ -161,6 +161,53 @@ func checkRefPath(p string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildPackage loads dir the way go test ./... does: build.Default, so
|
||||
// the host GOOS, GOARCH, CGO_ENABLED and release tags apply.
|
||||
func buildPackage(dir string) (*build.Package, error) {
|
||||
return build.ImportDir(dir, 0)
|
||||
}
|
||||
|
||||
// checkBuiltGo refuses a .go src= target go test ./... does not compile:
|
||||
// a testdata or underscore directory, a file left out of the default
|
||||
// build, or a non-test file in a package with no tests.
|
||||
func checkBuiltGo(refPath, real string, isTest bool) error {
|
||||
for _, seg := range strings.Split(refPath, "/") {
|
||||
if seg == "testdata" || strings.HasPrefix(seg, "_") {
|
||||
return snippetError("file is in a directory go test ./... skips (testdata or a _-prefixed directory)")
|
||||
}
|
||||
}
|
||||
pkg, err := buildPackage(filepath.Dir(real))
|
||||
if err != nil {
|
||||
var noGo *build.NoGoError
|
||||
if errors.As(err, &noGo) {
|
||||
return snippetError("file is excluded from the default build (build constraint or GOOS/GOARCH file-name suffix)")
|
||||
}
|
||||
return snippetError("cannot load package: " + firstLine(err.Error()))
|
||||
}
|
||||
base := filepath.Base(real)
|
||||
listed := func(groups ...[]string) bool {
|
||||
for _, g := range groups {
|
||||
if slices.Contains(g, base) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if isTest {
|
||||
if !listed(pkg.TestGoFiles, pkg.XTestGoFiles) {
|
||||
return snippetError("file is excluded from the default build (build constraint or GOOS/GOARCH file-name suffix)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !listed(pkg.GoFiles, pkg.CgoFiles) {
|
||||
return snippetError("file is excluded from the default build (build constraint or GOOS/GOARCH file-name suffix)")
|
||||
}
|
||||
if len(pkg.TestGoFiles) == 0 && len(pkg.XTestGoFiles) == 0 {
|
||||
return snippetError("package has no _test.go files, so go test does not cover it")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRootModule refuses a Go source that sits in a nested module (a
|
||||
// directory between it and the root holds a go.mod): root go test does not
|
||||
// run it.
|
||||
@@ -246,11 +293,9 @@ func extractIdent(file string, raw []byte, name string, isTest bool) (string, er
|
||||
if funcKey(d) != name {
|
||||
continue
|
||||
}
|
||||
if isTest {
|
||||
if err := checkTestIdent(file, name, true); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
// An Example with output is a root, so it is not also asked
|
||||
// to be reachable from some other test. Without output, go
|
||||
// test compiles it and never runs it.
|
||||
if d.Recv == nil && strings.HasPrefix(name, "Example") && isTest {
|
||||
if !exampleHasOutput(f, name) {
|
||||
return "", snippetError(name + " has no // Output: comment, so go test compiles it but never runs it")
|
||||
@@ -258,6 +303,11 @@ func extractIdent(file string, raw []byte, name string, isTest bool) (string, er
|
||||
body := string(raw[offset(d.Body.Lbrace)+1 : offset(d.Body.Rbrace)])
|
||||
return strings.Join(dedent(strings.Split(body, "\n")), "\n"), nil
|
||||
}
|
||||
if isTest {
|
||||
if err := checkTestIdent(file, name, true); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
start := d.Pos()
|
||||
if d.Doc != nil {
|
||||
start = d.Doc.Pos()
|
||||
@@ -342,25 +392,43 @@ type testGraph struct {
|
||||
files map[string]*ast.File // by path
|
||||
}
|
||||
|
||||
func isRoot(d *ast.FuncDecl) bool {
|
||||
return d.Recv == nil && (strings.HasPrefix(d.Name.Name, "Test") || strings.HasPrefix(d.Name.Name, "Example"))
|
||||
// isTestName is the cmd/go rule for a test function: "Test" alone, or
|
||||
// "Test" followed by a rune that is not a lowercase letter. Testable
|
||||
// is not a test. Benchmark and Fuzz are not roots.
|
||||
func isTestName(name string) bool {
|
||||
rest, ok := strings.CutPrefix(name, "Test")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if rest == "" {
|
||||
return true
|
||||
}
|
||||
r, _ := utf8.DecodeRuneInString(rest)
|
||||
return !unicode.IsLower(r)
|
||||
}
|
||||
|
||||
func loadTestGraph(dir string) (*testGraph, error) {
|
||||
paths, err := filepath.Glob(filepath.Join(dir, "*_test.go"))
|
||||
pkg, err := buildPackage(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("docsite: %w", err)
|
||||
var noGo *build.NoGoError
|
||||
if !errors.As(err, &noGo) {
|
||||
return nil, snippetError("cannot load package: " + firstLine(err.Error()))
|
||||
}
|
||||
pkg = &build.Package{}
|
||||
}
|
||||
g := &testGraph{fset: token.NewFileSet(), funcs: map[string][]*ast.FuncDecl{},
|
||||
reachable: map[string]bool{}, referenced: map[string]bool{}, files: map[string]*ast.File{}}
|
||||
byName := map[string][]string{} // plain or method name -> keys
|
||||
var queue []string
|
||||
for _, p := range paths {
|
||||
f, err := parser.ParseFile(g.fset, p, nil, 0)
|
||||
var files []*ast.File
|
||||
for _, name := range append(append([]string{}, pkg.TestGoFiles...), pkg.XTestGoFiles...) {
|
||||
p := filepath.Join(dir, name)
|
||||
f, err := parser.ParseFile(g.fset, p, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, snippetError("cannot parse " + filepath.Base(p) + ": " + firstLine(err.Error()))
|
||||
}
|
||||
g.files[p] = f
|
||||
files = append(files, f)
|
||||
for _, decl := range f.Decls {
|
||||
d, ok := decl.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
@@ -369,12 +437,22 @@ func loadTestGraph(dir string) (*testGraph, error) {
|
||||
key := funcKey(d)
|
||||
g.funcs[key] = append(g.funcs[key], d)
|
||||
byName[d.Name.Name] = append(byName[d.Name.Name], key)
|
||||
if isRoot(d) && !g.reachable[key] {
|
||||
if d.Recv == nil && isTestName(d.Name.Name) && !g.reachable[key] {
|
||||
g.reachable[key] = true
|
||||
queue = append(queue, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, ex := range doc.Examples(files...) {
|
||||
if ex.Output == "" && !ex.EmptyOutput {
|
||||
continue
|
||||
}
|
||||
key := "Example" + ex.Name
|
||||
if !g.reachable[key] {
|
||||
g.reachable[key] = true
|
||||
queue = append(queue, key)
|
||||
}
|
||||
}
|
||||
for len(queue) > 0 {
|
||||
key := queue[0]
|
||||
queue = queue[1:]
|
||||
@@ -401,7 +479,7 @@ func loadTestGraph(dir string) (*testGraph, error) {
|
||||
return g, nil
|
||||
}
|
||||
|
||||
const notRunMessage = "fragment is not inside a Test or Example function, or a function one of them calls, so go test does not run it"
|
||||
const notRunMessage = "fragment is not inside a Test or Example function (an Example counts only with an // Output: comment), or a function one of them calls, so go test does not run it"
|
||||
|
||||
// checkTestIdent requires a _test.go declaration to be run by go test: a
|
||||
// function reachable from a Test or Example, or a type, var or const that
|
||||
|
||||
Reference in New Issue
Block a user