internal/godoc: split package docs into new package pkgdoc [generated]

Isolate package docs scanning and extraction into a new package.
Generated by script below.

[git-generate]
cd internal/godoc
rf '
	# We want to end up with each package having its own toFS,
	# but to keep each step leaving a valid package, export toFS as ToFSPath
	# so it can be used as pkgdoc.ToFSPath after the move.
	# We will rewrite the uses left behind after the move.
	mv toFS ToFSPath

	mv newDirTree newDir
	mv Directory Dir
	mv Dir.listing Dir.List
	mv Dir.lookup Dir.Lookup
	mv \
		ToFSPath \
		Dir \
		Dir.Name \
		DirList \
		DirEntry \
		DirEntry.Name \
		Dir.Lookup \
		Dir.List \
		newDir \
		isPkgFile \
		isPkgDir \
		Dir.walk \
		walkDirs \
		parseFile \
		parseFiles \
		linePrefix \
		replaceLinePrefixCommentsWithBlankLine \
		dir.go

	mv stripExampleSuffix TrimExampleSuffix
	mv splitExampleName SplitExampleName
	mv poorMansImporter simpleImporter

	mv dirtrees_test.go dir_test.go

	mv \
		DocTree \
		NewDocTree \
		PageInfo \
		PageInfo.IsEmpty \
		PageInfoMode \
		NoFiltering \
		modeNames \
		PageInfoMode.String \
		GetPageInfoMode \
		DocTree.GetPageInfo \
		DocTree.includePath \
		simpleImporter \
		packageExports \
		funcsByName \
		funcsByName.Len \
		funcsByName.Swap \
		funcsByName.Less \
		collectExamples \
		globalNames \
		addNames \
		SplitExampleName \
		TrimExampleSuffix \
		startsWithUppercase \
		doc.go

	mv \
		TestIgnoredGoFiles \
		TestIssue5247 \
		doc_test.go

	mv dir.go dir_test.go doc.go doc_test.go golang.org/x/website/internal/pkgdoc

	# Add a new toFS and rewrite the uses left behind.
	add server.go:/^\)/ \
		// toFS returns the io/fs name for path (no leading slash). \
		func toFS(name string) string { \
			if name == "/" { \
				return "." \
			} \
			return path.Clean(strings.TrimPrefix(name, "/")) \
		}

	ex {
		import "golang.org/x/website/internal/pkgdoc"
		var x string
		pkgdoc.ToFSPath(x) -> toFS(x)
	}
'
rm dirtrees.go parser.go
cd ../pkgdoc
rf '
	# Finish toFS split
	mv ToFSPath toFS

	# Clean up API for package pkgdoc
	mv PageInfo Page
	mv DocTree.GetPageInfo Doc.Page

	mv DocTree Docs
	mv NewDocTree NewDocs

	mv PageInfoMode Mode
	mv GetPageInfoMode ParseMode
	mv NoFiltering ModeAll
	mv FlatDir ModeFlat
	mv AllMethods ModeMethods
	mv ShowSource ModeSrc
	mv NoTypeAssoc ModeBuiltin
'

Change-Id: I24384f40739af286c528beb06aa153843005a870
Reviewed-on: https://go-review.googlesource.com/c/website/+/296380
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/internal/godoc/astfuncs.go b/internal/godoc/astfuncs.go
index 9788d98..22e1bdc 100644
--- a/internal/godoc/astfuncs.go
+++ b/internal/godoc/astfuncs.go
@@ -19,18 +19,19 @@
 	"unicode"
 
 	"golang.org/x/website/internal/api"
+	"golang.org/x/website/internal/pkgdoc"
 	"golang.org/x/website/internal/texthtml"
 )
 
 var slashSlash = []byte("//")
 
-func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string {
+func (p *Presentation) nodeFunc(info *pkgdoc.Page, node interface{}) string {
 	var buf bytes.Buffer
 	p.writeNode(&buf, info, info.FSet, node)
 	return buf.String()
 }
 
-func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
+func (p *Presentation) node_htmlFunc(info *pkgdoc.Page, node interface{}, linkify bool) string {
 	var buf1 bytes.Buffer
 	p.writeNode(&buf1, info, info.FSet, node)
 
@@ -53,7 +54,7 @@
 // The provided fset must be non-nil. The pageInfo is optional. If
 // present, the pageInfo is used to add comments to struct fields to
 // say which version of Go introduced them.
-func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) {
+func (p *Presentation) writeNode(w io.Writer, pageInfo *pkgdoc.Page, fset *token.FileSet, x interface{}) {
 	// convert trailing tabs into spaces using a tconv filter
 	// to ensure a good outcome in most browsers (there may still
 	// be tabs in comments and strings, but converting those into
diff --git a/internal/godoc/examplefuncs.go b/internal/godoc/examplefuncs.go
index 127bd17..3d10fc1 100644
--- a/internal/godoc/examplefuncs.go
+++ b/internal/godoc/examplefuncs.go
@@ -15,14 +15,15 @@
 	"log"
 	"regexp"
 	"strings"
-	"unicode"
 	"unicode/utf8"
+
+	"golang.org/x/website/internal/pkgdoc"
 )
 
-func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string {
+func (p *Presentation) example_htmlFunc(info *pkgdoc.Page, funcName string) string {
 	var buf bytes.Buffer
 	for _, eg := range info.Examples {
-		name := stripExampleSuffix(eg.Name)
+		name := pkgdoc.TrimExampleSuffix(eg.Name)
 
 		if name != funcName {
 			continue
@@ -190,7 +191,7 @@
 // example_nameFunc takes an example function name and returns its display
 // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
 func (p *Presentation) example_nameFunc(s string) string {
-	name, suffix := splitExampleName(s)
+	name, suffix := pkgdoc.SplitExampleName(s)
 	// replace _ with . for method names
 	name = strings.Replace(name, "_", ".", 1)
 	// use "Package" if no name provided
@@ -203,33 +204,6 @@
 // example_suffixFunc takes an example function name and returns its suffix in
 // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
 func (p *Presentation) example_suffixFunc(name string) string {
-	_, suffix := splitExampleName(name)
+	_, suffix := pkgdoc.SplitExampleName(name)
 	return suffix
 }
-
-func splitExampleName(s string) (name, suffix string) {
-	i := strings.LastIndex(s, "_")
-	if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
-		name = s[:i]
-		suffix = " (" + strings.Title(s[i+1:]) + ")"
-		return
-	}
-	name = s
-	return
-}
-
-// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
-// while keeping uppercase Braz in Foo_Braz.
-func stripExampleSuffix(name string) string {
-	if i := strings.LastIndex(name, "_"); i != -1 {
-		if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
-			name = name[:i]
-		}
-	}
-	return name
-}
-
-func startsWithUppercase(s string) bool {
-	r, _ := utf8.DecodeRuneInString(s)
-	return unicode.IsUpper(r)
-}
diff --git a/internal/godoc/godoc.go b/internal/godoc/godoc.go
index 7117b5a..3747b03 100644
--- a/internal/godoc/godoc.go
+++ b/internal/godoc/godoc.go
@@ -17,6 +17,8 @@
 	"strconv"
 	"strings"
 	"text/template"
+
+	"golang.org/x/website/internal/pkgdoc"
 )
 
 // FuncMap defines template functions used in godoc templates.
@@ -81,31 +83,6 @@
 	return localname
 }
 
-type PageInfo struct {
-	Dirname  string // directory containing the package
-	Err      error  // error or nil
-	GoogleCN bool   // page is being served from golang.google.cn
-
-	Mode PageInfoMode // display metadata from query string
-
-	// package info
-	FSet       *token.FileSet       // nil if no package documentation
-	PDoc       *doc.Package         // nil if no package documentation
-	Examples   []*doc.Example       // nil if no example code
-	Bugs       []*doc.Note          // nil if no BUG comments
-	PAst       map[string]*ast.File // nil if no AST with package exports
-	IsMain     bool                 // true for package main
-	IsFiltered bool                 // true if results were filtered
-
-	// directory info
-	Dirs    *DirList // nil if no directory information
-	DirFlat bool     // if set, show directory in a flat (non-indented) manner
-}
-
-func (info *PageInfo) IsEmpty() bool {
-	return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
-}
-
 func pkgLinkFunc(path string) string {
 	// because of the irregular mapping under goroot
 	// we need to correct certain relative paths
@@ -157,7 +134,7 @@
 	return buf.String()
 }
 
-func posLink_urlFunc(info *PageInfo, n interface{}) string {
+func posLink_urlFunc(info *pkgdoc.Page, n interface{}) string {
 	// n must be an ast.Node or a *doc.Note
 	var pos, end token.Pos
 
diff --git a/internal/godoc/godoc_test.go b/internal/godoc/godoc_test.go
index d4c21cb..74e472c 100644
--- a/internal/godoc/godoc_test.go
+++ b/internal/godoc/godoc_test.go
@@ -14,6 +14,8 @@
 	"go/token"
 	"strings"
 	"testing"
+
+	"golang.org/x/website/internal/pkgdoc"
 )
 
 func TestPkgLinkFunc(t *testing.T) {
@@ -236,7 +238,7 @@
 		t.Fatal(err)
 	}
 	var buf bytes.Buffer
-	pi := &PageInfo{
+	pi := &pkgdoc.Page{
 		FSet: fset,
 	}
 	sep := ""
diff --git a/internal/godoc/parser.go b/internal/godoc/parser.go
deleted file mode 100644
index b375ddb..0000000
--- a/internal/godoc/parser.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2011 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.
-
-//go:build go1.16
-// +build go1.16
-
-// This file contains support functions for parsing .go files
-// accessed via godoc's file system fs.
-
-package godoc
-
-import (
-	"bytes"
-	"go/ast"
-	"go/parser"
-	"go/token"
-	"io/fs"
-	"path"
-)
-
-var linePrefix = []byte("//line ")
-
-// This function replaces source lines starting with "//line " with a blank line.
-// It does this irrespective of whether the line is truly a line comment or not;
-// e.g., the line may be inside a string, or a /*-style comment; however that is
-// rather unlikely (proper testing would require a full Go scan which we want to
-// avoid for performance).
-func replaceLinePrefixCommentsWithBlankLine(src []byte) {
-	for {
-		i := bytes.Index(src, linePrefix)
-		if i < 0 {
-			break // we're done
-		}
-		// 0 <= i && i+len(linePrefix) <= len(src)
-		if i == 0 || src[i-1] == '\n' {
-			// at beginning of line: blank out line
-			for i < len(src) && src[i] != '\n' {
-				src[i] = ' '
-				i++
-			}
-		} else {
-			// not at beginning of line: skip over prefix
-			i += len(linePrefix)
-		}
-		// i <= len(src)
-		src = src[i:]
-	}
-}
-
-func parseFile(fsys fs.FS, fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) {
-	src, err := fs.ReadFile(fsys, toFS(filename))
-	if err != nil {
-		return nil, err
-	}
-
-	// Temporary ad-hoc fix for issue 5247.
-	// TODO(gri,dmitshur) Remove this in favor of a better fix, eventually (see issue 32092).
-	replaceLinePrefixCommentsWithBlankLine(src)
-
-	return parser.ParseFile(fset, filename, src, mode)
-}
-
-func parseFiles(fsys fs.FS, fset *token.FileSet, relpath string, abspath string, localnames []string) (map[string]*ast.File, error) {
-	files := make(map[string]*ast.File)
-	for _, f := range localnames {
-		absname := path.Join(abspath, f)
-		file, err := parseFile(fsys, fset, absname, parser.ParseComments)
-		if err != nil {
-			return nil, err
-		}
-		files[path.Join(relpath, f)] = file
-	}
-
-	return files, nil
-}
diff --git a/internal/godoc/pres.go b/internal/godoc/pres.go
index 2689c4a..4aaa41b 100644
--- a/internal/godoc/pres.go
+++ b/internal/godoc/pres.go
@@ -11,6 +11,8 @@
 	"net/http"
 	"sync"
 	"text/template"
+
+	"golang.org/x/website/internal/pkgdoc"
 )
 
 // Presentation generates output from a corpus.
@@ -52,7 +54,7 @@
 	}
 	docs := &docServer{
 		p: p,
-		d: NewDocTree(c.fs),
+		d: pkgdoc.NewDocs(c.fs),
 	}
 	p.mux.Handle("/cmd/", docs)
 	p.mux.Handle("/pkg/", docs)
diff --git a/internal/godoc/server.go b/internal/godoc/server.go
index 8547bcf..883ef0f 100644
--- a/internal/godoc/server.go
+++ b/internal/godoc/server.go
@@ -11,216 +11,36 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"go/ast"
-	"go/build"
-	"go/doc"
-	"go/token"
 	htmlpkg "html"
 	"io"
 	"io/fs"
-	"io/ioutil"
 	"log"
 	"net/http"
-	"os"
 	"path"
-	"path/filepath"
 	"regexp"
-	"sort"
 	"strconv"
 	"strings"
 	"text/template"
 
+	"golang.org/x/website/internal/pkgdoc"
 	"golang.org/x/website/internal/spec"
 	"golang.org/x/website/internal/texthtml"
 )
 
-type DocTree struct {
-	fs   fs.FS
-	root *Directory
-}
-
-func NewDocTree(fsys fs.FS) *DocTree {
-	src := newDirTree(fsys, token.NewFileSet(), "/src")
-	root := &Directory{
-		Path: "/",
-		Dirs: []*Directory{src},
+// toFS returns the io/fs name for path (no leading slash).
+func toFS(name string) string {
+	if name == "/" {
+		return "."
 	}
-	return &DocTree{
-		fs:   fsys,
-		root: root,
-	}
+	return path.Clean(strings.TrimPrefix(name, "/"))
 }
 
 // docServer serves a package doc tree (/cmd or /pkg).
 type docServer struct {
 	p *Presentation
-	d *DocTree
+	d *pkgdoc.Docs
 }
 
-// GetPageInfo returns the PageInfo for a package directory abspath. If the
-// parameter genAST is set, an AST containing only the package exports is
-// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
-// is extracted from the AST. If there is no corresponding package in the
-// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
-// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
-// set to the respective error but the error is not logged.
-func (d *DocTree) GetPageInfo(abspath, relpath string, mode PageInfoMode, goos, goarch string) *PageInfo {
-	info := &PageInfo{Dirname: abspath, Mode: mode}
-
-	// Restrict to the package files that would be used when building
-	// the package on this system.  This makes sure that if there are
-	// separate implementations for, say, Windows vs Unix, we don't
-	// jumble them all together.
-	// Note: If goos/goarch aren't set, the current binary's GOOS/GOARCH
-	// are used.
-	ctxt := build.Default
-	ctxt.IsAbsPath = path.IsAbs
-	ctxt.IsDir = func(path string) bool {
-		fi, err := fs.Stat(d.fs, toFS(filepath.ToSlash(path)))
-		return err == nil && fi.IsDir()
-	}
-	ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
-		f, err := fs.ReadDir(d.fs, toFS(filepath.ToSlash(dir)))
-		filtered := make([]os.FileInfo, 0, len(f))
-		for _, i := range f {
-			if mode&NoFiltering != 0 || i.Name() != "internal" {
-				info, err := i.Info()
-				if err == nil {
-					filtered = append(filtered, info)
-				}
-			}
-		}
-		return filtered, err
-	}
-	ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
-		data, err := fs.ReadFile(d.fs, toFS(filepath.ToSlash(name)))
-		if err != nil {
-			return nil, err
-		}
-		return ioutil.NopCloser(bytes.NewReader(data)), nil
-	}
-
-	// Make the syscall/js package always visible by default.
-	// It defaults to the host's GOOS/GOARCH, and golang.org's
-	// linux/amd64 means the wasm syscall/js package was blank.
-	// And you can't run godoc on js/wasm anyway, so host defaults
-	// don't make sense here.
-	if goos == "" && goarch == "" && relpath == "syscall/js" {
-		goos, goarch = "js", "wasm"
-	}
-	if goos != "" {
-		ctxt.GOOS = goos
-	}
-	if goarch != "" {
-		ctxt.GOARCH = goarch
-	}
-
-	pkginfo, err := ctxt.ImportDir(abspath, 0)
-	// continue if there are no Go source files; we still want the directory info
-	if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
-		info.Err = err
-		return info
-	}
-
-	// collect package files
-	pkgname := pkginfo.Name
-	pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
-	if len(pkgfiles) == 0 {
-		// Commands written in C have no .go files in the build.
-		// Instead, documentation may be found in an ignored file.
-		// The file may be ignored via an explicit +build ignore
-		// constraint (recommended), or by defining the package
-		// documentation (historic).
-		pkgname = "main" // assume package main since pkginfo.Name == ""
-		pkgfiles = pkginfo.IgnoredGoFiles
-	}
-
-	// get package information, if any
-	if len(pkgfiles) > 0 {
-		// build package AST
-		fset := token.NewFileSet()
-		files, err := parseFiles(d.fs, fset, relpath, abspath, pkgfiles)
-		if err != nil {
-			info.Err = err
-			return info
-		}
-
-		// ignore any errors - they are due to unresolved identifiers
-		pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
-
-		// extract package documentation
-		info.FSet = fset
-		if mode&ShowSource == 0 {
-			// show extracted documentation
-			var m doc.Mode
-			if mode&NoFiltering != 0 {
-				m |= doc.AllDecls
-			}
-			if mode&AllMethods != 0 {
-				m |= doc.AllMethods
-			}
-			info.PDoc = doc.New(pkg, path.Clean(relpath), m) // no trailing '/' in importpath
-			if mode&NoTypeAssoc != 0 {
-				for _, t := range info.PDoc.Types {
-					info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
-					info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
-					info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
-					t.Consts = nil
-					t.Vars = nil
-					t.Funcs = nil
-				}
-				// for now we cannot easily sort consts and vars since
-				// go/doc.Value doesn't export the order information
-				sort.Sort(funcsByName(info.PDoc.Funcs))
-			}
-
-			// collect examples
-			testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
-			files, err = parseFiles(d.fs, fset, relpath, abspath, testfiles)
-			if err != nil {
-				log.Println("parsing examples:", err)
-			}
-			info.Examples = collectExamples(pkg, files)
-			info.Bugs = info.PDoc.Notes["BUG"]
-		} else {
-			// show source code
-			// TODO(gri) Consider eliminating export filtering in this mode,
-			//           or perhaps eliminating the mode altogether.
-			if mode&NoFiltering == 0 {
-				packageExports(fset, pkg)
-			}
-			info.PAst = files
-		}
-		info.IsMain = pkgname == "main"
-	}
-
-	info.Dirs = d.root.lookup(abspath).listing(func(path string) bool { return d.includePath(path, mode) })
-	info.DirFlat = mode&FlatDir != 0
-
-	return info
-}
-
-func (d *DocTree) includePath(path string, mode PageInfoMode) (r bool) {
-	// if the path includes 'internal', don't list unless we are in the NoFiltering mode.
-	if mode&NoFiltering != 0 {
-		return true
-	}
-	if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
-		for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
-			if c == "internal" || c == "vendor" {
-				return false
-			}
-		}
-	}
-	return true
-}
-
-type funcsByName []*doc.Func
-
-func (s funcsByName) Len() int           { return len(s) }
-func (s funcsByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
-func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
-
 func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if redirect(w, r) {
 		return
@@ -230,14 +50,14 @@
 	relpath := path.Clean(strings.TrimPrefix(r.URL.Path, "/pkg/"))
 
 	abspath := path.Join("/src", relpath)
-	mode := GetPageInfoMode(r.FormValue("m"))
+	mode := pkgdoc.ParseMode(r.FormValue("m"))
 	if relpath == "builtin" {
 		// The fake built-in package contains unexported identifiers,
 		// but we want to show them. Also, disable type association,
 		// since it's not helpful for this fake package (see issue 6645).
-		mode |= NoFiltering | NoTypeAssoc
+		mode |= pkgdoc.ModeAll | pkgdoc.ModeBuiltin
 	}
-	info := h.d.GetPageInfo(abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
+	info := pkgdoc.Doc(h.d, abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
 	if info.Err != nil {
 		log.Print(info.Err)
 		h.p.ServeError(w, r, relpath, info.Err)
@@ -294,40 +114,7 @@
 	})
 }
 
-type PageInfoMode uint
-
-const (
-	NoFiltering PageInfoMode = 1 << iota // do not filter exports
-	FlatDir                              // show directory in a flat (non-indented) manner
-	AllMethods                           // show all embedded methods
-	ShowSource                           // show source code, do not extract documentation
-	NoTypeAssoc                          // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
-)
-
-// modeNames defines names for each PageInfoMode flag.
-// The order here must match the order of the constants above.
-var modeNames = []string{
-	"all",
-	"flat",
-	"methods",
-	"src",
-}
-
-// generate a query string for persisting PageInfoMode between pages.
-func (m PageInfoMode) String() string {
-	s := ""
-	for i, name := range modeNames {
-		if m&(1<<i) != 0 && name != "" {
-			if s != "" {
-				s += ","
-			}
-			s += name
-		}
-	}
-	return s
-}
-
-func modeQueryString(m PageInfoMode) string {
+func modeQueryString(m pkgdoc.Mode) string {
 	s := m.String()
 	if s == "" {
 		return ""
@@ -335,113 +122,6 @@
 	return "?m=" + s
 }
 
-// GetPageInfoMode computes the PageInfoMode flags by analyzing the request
-// URL form value "m". It is value is a comma-separated list of mode names (for example, "all,flat").
-func GetPageInfoMode(text string) PageInfoMode {
-	var mode PageInfoMode
-	for _, k := range strings.Split(text, ",") {
-		k = strings.TrimSpace(k)
-		for i, name := range modeNames {
-			if name == k {
-				mode |= 1 << i
-			}
-		}
-	}
-	return mode
-}
-
-// poorMansImporter returns a (dummy) package object named
-// by the last path component of the provided package path
-// (as is the convention for packages). This is sufficient
-// to resolve package identifiers without doing an actual
-// import. It never returns an error.
-//
-func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
-	pkg := imports[path]
-	if pkg == nil {
-		// note that strings.LastIndex returns -1 if there is no "/"
-		pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
-		pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
-		imports[path] = pkg
-	}
-	return pkg, nil
-}
-
-// globalNames returns a set of the names declared by all package-level
-// declarations. Method names are returned in the form Receiver_Method.
-func globalNames(pkg *ast.Package) map[string]bool {
-	names := make(map[string]bool)
-	for _, file := range pkg.Files {
-		for _, decl := range file.Decls {
-			addNames(names, decl)
-		}
-	}
-	return names
-}
-
-// collectExamples collects examples for pkg from testfiles.
-func collectExamples(pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
-	var files []*ast.File
-	for _, f := range testfiles {
-		files = append(files, f)
-	}
-
-	var examples []*doc.Example
-	globals := globalNames(pkg)
-	for _, e := range doc.Examples(files...) {
-		name := stripExampleSuffix(e.Name)
-		if name == "" || globals[name] {
-			examples = append(examples, e)
-		}
-	}
-
-	return examples
-}
-
-// addNames adds the names declared by decl to the names set.
-// Method names are added in the form ReceiverTypeName_Method.
-func addNames(names map[string]bool, decl ast.Decl) {
-	switch d := decl.(type) {
-	case *ast.FuncDecl:
-		name := d.Name.Name
-		if d.Recv != nil {
-			var typeName string
-			switch r := d.Recv.List[0].Type.(type) {
-			case *ast.StarExpr:
-				typeName = r.X.(*ast.Ident).Name
-			case *ast.Ident:
-				typeName = r.Name
-			}
-			name = typeName + "_" + name
-		}
-		names[name] = true
-	case *ast.GenDecl:
-		for _, spec := range d.Specs {
-			switch s := spec.(type) {
-			case *ast.TypeSpec:
-				names[s.Name.Name] = true
-			case *ast.ValueSpec:
-				for _, id := range s.Names {
-					names[id.Name] = true
-				}
-			}
-		}
-	}
-}
-
-// packageExports is a local implementation of ast.PackageExports
-// which correctly updates each package file's comment list.
-// (The ast.PackageExports signature is frozen, hence the local
-// implementation).
-//
-func packageExports(fset *token.FileSet, pkg *ast.Package) {
-	for _, src := range pkg.Files {
-		cmap := ast.NewCommentMap(fset, src, src.Comments)
-		ast.FileExports(src)
-		src.Comments = cmap.Filter(src).Comments()
-	}
-}
-
 func applyTemplate(t *template.Template, name string, data interface{}) []byte {
 	var buf bytes.Buffer
 	if err := t.Execute(&buf, data); err != nil {
diff --git a/internal/godoc/server_test.go b/internal/godoc/server_test.go
index 934850e..a0ca02b 100644
--- a/internal/godoc/server_test.go
+++ b/internal/godoc/server_test.go
@@ -17,58 +17,6 @@
 	"text/template"
 )
 
-// TestIgnoredGoFiles tests the scenario where a folder has no .go or .c files,
-// but has an ignored go file.
-func TestIgnoredGoFiles(t *testing.T) {
-	packagePath := "github.com/package"
-	packageComment := "main is documented in an ignored .go file"
-
-	fs := fstest.MapFS{
-		"src/" + packagePath + "/ignored.go": {Data: []byte(`// +build ignore
-
-// ` + packageComment + `
-package main`)},
-	}
-	d := NewDocTree(fs)
-	pInfo := d.GetPageInfo("/src/"+packagePath, packagePath, NoFiltering, "linux", "amd64")
-
-	if pInfo.PDoc == nil {
-		t.Error("pInfo.PDoc = nil; want non-nil.")
-	} else {
-		if got, want := pInfo.PDoc.Doc, packageComment+"\n"; got != want {
-			t.Errorf("pInfo.PDoc.Doc = %q; want %q.", got, want)
-		}
-		if got, want := pInfo.PDoc.Name, "main"; got != want {
-			t.Errorf("pInfo.PDoc.Name = %q; want %q.", got, want)
-		}
-		if got, want := pInfo.PDoc.ImportPath, packagePath; got != want {
-			t.Errorf("pInfo.PDoc.ImportPath = %q; want %q.", got, want)
-		}
-	}
-	if pInfo.FSet == nil {
-		t.Error("pInfo.FSet = nil; want non-nil.")
-	}
-}
-
-func TestIssue5247(t *testing.T) {
-	const packagePath = "example.com/p"
-	fs := fstest.MapFS{
-		"src/" + packagePath + "/p.go": {Data: []byte(`package p
-
-//line notgen.go:3
-// F doc //line 1 should appear
-// line 2 should appear
-func F()
-//line foo.go:100`)}, // No newline at end to check corner cases.
-	}
-
-	d := NewDocTree(fs)
-	pInfo := d.GetPageInfo("/src/"+packagePath, packagePath, 0, "linux", "amd64")
-	if got, want := pInfo.PDoc.Funcs[0].Doc, "F doc //line 1 should appear\nline 2 should appear\n"; got != want {
-		t.Errorf("pInfo.PDoc.Funcs[0].Doc = %q; want %q", got, want)
-	}
-}
-
 func testServeBody(t *testing.T, p *Presentation, path, body string) {
 	t.Helper()
 	r := &http.Request{URL: &url.URL{Path: path}}
diff --git a/internal/godoc/dirtrees.go b/internal/pkgdoc/dir.go
similarity index 67%
rename from internal/godoc/dirtrees.go
rename to internal/pkgdoc/dir.go
index dde3ff1..6a10baf 100644
--- a/internal/godoc/dirtrees.go
+++ b/internal/pkgdoc/dir.go
@@ -7,9 +7,11 @@
 
 // This file contains the code dealing with package directory trees.
 
-package godoc
+package pkgdoc
 
 import (
+	"bytes"
+	"go/ast"
 	"go/doc"
 	"go/parser"
 	"go/token"
@@ -20,32 +22,111 @@
 	"strings"
 )
 
-type Directory struct {
-	Path     string       // directory path
-	HasPkg   bool         // true if the directory contains at least one package
-	Synopsis string       // package documentation, if any
-	Dirs     []*Directory // subdirectories
+// toFS returns the io/fs name for path (no leading slash).
+func toFS(name string) string {
+	if name == "/" {
+		return "."
+	}
+	return path.Clean(strings.TrimPrefix(name, "/"))
 }
 
-func (d *Directory) Name() string {
+type Dir struct {
+	Path     string // directory path
+	HasPkg   bool   // true if the directory contains at least one package
+	Synopsis string // package documentation, if any
+	Dirs     []*Dir // subdirectories
+}
+
+func (d *Dir) Name() string {
 	return path.Base(d.Path)
 }
 
-func isPkgFile(fi fs.DirEntry) bool {
-	name := fi.Name()
-	return !fi.IsDir() &&
-		path.Ext(name) == ".go" &&
-		!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
+type DirList struct {
+	List []DirEntry
 }
 
-func isPkgDir(fi fs.DirEntry) bool {
-	name := fi.Name()
-	return fi.IsDir() &&
-		name != "testdata" &&
-		len(name) > 0 && name[0] != '_' && name[0] != '.' // ignore _files and .files
+// DirEntry describes a directory entry.
+// The Depth gives the directory depth relative to the overall list,
+// for use in presenting a hierarchical directory entry.
+type DirEntry struct {
+	Depth    int    // >= 0
+	Path     string // relative path to directory from listing start
+	HasPkg   bool   // true if the directory contains at least one package
+	Synopsis string // package documentation, if any
 }
 
-func newDirTree(fsys fs.FS, fset *token.FileSet, abspath string) *Directory {
+func (d *DirEntry) Name() string {
+	return path.Base(d.Path)
+}
+
+// Lookup looks for the *Directory for a given named path, relative to dir.
+func (dir *Dir) Lookup(name string) *Dir {
+	name = path.Join(dir.Path, name)
+	if name == dir.Path {
+		return dir
+	}
+	dirPathLen := len(dir.Path)
+	if dir.Path == "/" {
+		dirPathLen = 0 // so path[dirPathLen] is a slash
+	}
+	if !strings.HasPrefix(name, dir.Path) || name[dirPathLen] != '/' {
+		println("NO", name, dir.Path)
+		return nil
+	}
+	d := dir
+Walk:
+	for i := dirPathLen + 1; i <= len(name); i++ {
+		if i == len(name) || name[i] == '/' {
+			// Find next child along path.
+			for _, sub := range d.Dirs {
+				if sub.Path == name[:i] {
+					d = sub
+					continue Walk
+				}
+			}
+			println("LOST", name[:i])
+			return nil
+		}
+	}
+	return d
+}
+
+// List creates a (linear) directory List from a directory tree.
+// If skipRoot is set, the root directory itself is excluded from the list.
+// If filter is set, only the directory entries whose paths match the filter
+// are included.
+//
+func (dir *Dir) List(filter func(string) bool) *DirList {
+	if dir == nil {
+		return nil
+	}
+
+	var list []DirEntry
+	dir.walk(func(d *Dir, depth int) {
+		if depth == 0 || filter != nil && !filter(d.Path) {
+			return
+		}
+		// the path is relative to root.Path - remove the root.Path
+		// prefix (the prefix should always be present but avoid
+		// crashes and check)
+		path := strings.TrimPrefix(d.Path, dir.Path)
+		// remove leading separator if any - path must be relative
+		path = strings.TrimPrefix(path, "/")
+		list = append(list, DirEntry{
+			Depth:    depth,
+			Path:     path,
+			HasPkg:   d.HasPkg,
+			Synopsis: d.Synopsis,
+		})
+	})
+
+	if len(list) == 0 {
+		return nil
+	}
+	return &DirList{list}
+}
+
+func newDir(fsys fs.FS, fset *token.FileSet, abspath string) *Dir {
 	var synopses [3]string // prioritized package documentation (0 == highest priority)
 
 	hasPkgFiles := false
@@ -58,15 +139,15 @@
 	}
 
 	// determine number of subdirectories and if there are package files
-	var dirchs []chan *Directory
-	var dirs []*Directory
+	var dirchs []chan *Dir
+	var dirs []*Dir
 
 	for _, d := range list {
 		name := d.Name()
 		filename := path.Join(abspath, name)
 		switch {
 		case isPkgDir(d):
-			dir := newDirTree(fsys, fset, filename)
+			dir := newDir(fsys, fset, filename)
 			if dir != nil {
 				dirs = append(dirs, dir)
 			}
@@ -130,7 +211,7 @@
 		}
 	}
 
-	return &Directory{
+	return &Dir{
 		Path:     abspath,
 		HasPkg:   hasPkgFiles,
 		Synopsis: synopsis,
@@ -138,109 +219,86 @@
 	}
 }
 
-// toFS returns the io/fs name for path (no leading slash).
-func toFS(name string) string {
-	if name == "/" {
-		return "."
-	}
-	return path.Clean(strings.TrimPrefix(name, "/"))
+func isPkgFile(fi fs.DirEntry) bool {
+	name := fi.Name()
+	return !fi.IsDir() &&
+		path.Ext(name) == ".go" &&
+		!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
+}
+
+func isPkgDir(fi fs.DirEntry) bool {
+	name := fi.Name()
+	return fi.IsDir() &&
+		name != "testdata" &&
+		len(name) > 0 && name[0] != '_' && name[0] != '.' // ignore _files and .files
 }
 
 // walk calls f(d, depth) for each directory d in the tree rooted at dir, including dir itself.
 // The depth argument specifies the depth of d in the tree.
 // The depth of dir itself is 0.
-func (dir *Directory) walk(f func(d *Directory, depth int)) {
+func (dir *Dir) walk(f func(d *Dir, depth int)) {
 	walkDirs(f, dir, 0)
 }
 
-func walkDirs(f func(d *Directory, depth int), d *Directory, depth int) {
+func walkDirs(f func(d *Dir, depth int), d *Dir, depth int) {
 	f(d, depth)
 	for _, sub := range d.Dirs {
 		walkDirs(f, sub, depth+1)
 	}
 }
 
-// lookup looks for the *Directory for a given named path, relative to dir.
-func (dir *Directory) lookup(name string) *Directory {
-	name = path.Join(dir.Path, name)
-	if name == dir.Path {
-		return dir
+func parseFile(fsys fs.FS, fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) {
+	src, err := fs.ReadFile(fsys, toFS(filename))
+	if err != nil {
+		return nil, err
 	}
-	dirPathLen := len(dir.Path)
-	if dir.Path == "/" {
-		dirPathLen = 0 // so path[dirPathLen] is a slash
+
+	// Temporary ad-hoc fix for issue 5247.
+	// TODO(gri,dmitshur) Remove this in favor of a better fix, eventually (see issue 32092).
+	replaceLinePrefixCommentsWithBlankLine(src)
+
+	return parser.ParseFile(fset, filename, src, mode)
+}
+
+func parseFiles(fsys fs.FS, fset *token.FileSet, relpath string, abspath string, localnames []string) (map[string]*ast.File, error) {
+	files := make(map[string]*ast.File)
+	for _, f := range localnames {
+		absname := path.Join(abspath, f)
+		file, err := parseFile(fsys, fset, absname, parser.ParseComments)
+		if err != nil {
+			return nil, err
+		}
+		files[path.Join(relpath, f)] = file
 	}
-	if !strings.HasPrefix(name, dir.Path) || name[dirPathLen] != '/' {
-		println("NO", name, dir.Path)
-		return nil
-	}
-	d := dir
-Walk:
-	for i := dirPathLen + 1; i <= len(name); i++ {
-		if i == len(name) || name[i] == '/' {
-			// Find next child along path.
-			for _, sub := range d.Dirs {
-				if sub.Path == name[:i] {
-					d = sub
-					continue Walk
-				}
+
+	return files, nil
+}
+
+var linePrefix = []byte("//line ")
+
+// This function replaces source lines starting with "//line " with a blank line.
+// It does this irrespective of whether the line is truly a line comment or not;
+// e.g., the line may be inside a string, or a /*-style comment; however that is
+// rather unlikely (proper testing would require a full Go scan which we want to
+// avoid for performance).
+func replaceLinePrefixCommentsWithBlankLine(src []byte) {
+	for {
+		i := bytes.Index(src, linePrefix)
+		if i < 0 {
+			break // we're done
+		}
+		// 0 <= i && i+len(linePrefix) <= len(src)
+		if i == 0 || src[i-1] == '\n' {
+			// at beginning of line: blank out line
+			for i < len(src) && src[i] != '\n' {
+				src[i] = ' '
+				i++
 			}
-			println("LOST", name[:i])
-			return nil
+		} else {
+			// not at beginning of line: skip over prefix
+			i += len(linePrefix)
 		}
+		// i <= len(src)
+		src = src[i:]
 	}
-	return d
-}
-
-// DirEntry describes a directory entry.
-// The Depth gives the directory depth relative to the overall list,
-// for use in presenting a hierarchical directory entry.
-type DirEntry struct {
-	Depth    int    // >= 0
-	Path     string // relative path to directory from listing start
-	HasPkg   bool   // true if the directory contains at least one package
-	Synopsis string // package documentation, if any
-}
-
-func (d *DirEntry) Name() string {
-	return path.Base(d.Path)
-}
-
-type DirList struct {
-	List []DirEntry
-}
-
-// listing creates a (linear) directory listing from a directory tree.
-// If skipRoot is set, the root directory itself is excluded from the list.
-// If filter is set, only the directory entries whose paths match the filter
-// are included.
-//
-func (dir *Directory) listing(filter func(string) bool) *DirList {
-	if dir == nil {
-		return nil
-	}
-
-	var list []DirEntry
-	dir.walk(func(d *Directory, depth int) {
-		if depth == 0 || filter != nil && !filter(d.Path) {
-			return
-		}
-		// the path is relative to root.Path - remove the root.Path
-		// prefix (the prefix should always be present but avoid
-		// crashes and check)
-		path := strings.TrimPrefix(d.Path, dir.Path)
-		// remove leading separator if any - path must be relative
-		path = strings.TrimPrefix(path, "/")
-		list = append(list, DirEntry{
-			Depth:    depth,
-			Path:     path,
-			HasPkg:   d.HasPkg,
-			Synopsis: d.Synopsis,
-		})
-	})
-
-	if len(list) == 0 {
-		return nil
-	}
-	return &DirList{list}
 }
diff --git a/internal/godoc/dirtrees_test.go b/internal/pkgdoc/dir_test.go
similarity index 81%
rename from internal/godoc/dirtrees_test.go
rename to internal/pkgdoc/dir_test.go
index e228901..b7b56d5 100644
--- a/internal/godoc/dirtrees_test.go
+++ b/internal/pkgdoc/dir_test.go
@@ -5,7 +5,7 @@
 //go:build go1.16
 // +build go1.16
 
-package godoc
+package pkgdoc
 
 import (
 	"go/token"
@@ -16,11 +16,11 @@
 )
 
 func TestNewDirTree(t *testing.T) {
-	dir := newDirTree(os.DirFS(runtime.GOROOT()), token.NewFileSet(), "/src")
+	dir := newDir(os.DirFS(runtime.GOROOT()), token.NewFileSet(), "/src")
 	processDir(t, dir)
 }
 
-func processDir(t *testing.T, dir *Directory) {
+func processDir(t *testing.T, dir *Dir) {
 	var list []string
 	for _, d := range dir.Dirs {
 		list = append(list, d.Name())
@@ -43,6 +43,6 @@
 	b.ResetTimer()
 	b.ReportAllocs()
 	for tries := 0; tries < b.N; tries++ {
-		newDirTree(fs, token.NewFileSet(), "/src")
+		newDir(fs, token.NewFileSet(), "/src")
 	}
 }
diff --git a/internal/pkgdoc/doc.go b/internal/pkgdoc/doc.go
new file mode 100644
index 0000000..219e673
--- /dev/null
+++ b/internal/pkgdoc/doc.go
@@ -0,0 +1,402 @@
+// Copyright 2013 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.
+
+//go:build go1.16
+// +build go1.16
+
+package pkgdoc
+
+import (
+	"bytes"
+	"go/ast"
+	"go/build"
+	"go/doc"
+	"go/token"
+	"io"
+	"io/fs"
+	"io/ioutil"
+	"log"
+	"os"
+	"path"
+	"path/filepath"
+	"sort"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+type Docs struct {
+	fs   fs.FS
+	root *Dir
+}
+
+func NewDocs(fsys fs.FS) *Docs {
+	src := newDir(fsys, token.NewFileSet(), "/src")
+	root := &Dir{
+		Path: "/",
+		Dirs: []*Dir{src},
+	}
+	return &Docs{
+		fs:   fsys,
+		root: root,
+	}
+}
+
+type Page struct {
+	Dirname  string // directory containing the package
+	Err      error  // error or nil
+	GoogleCN bool   // page is being served from golang.google.cn
+
+	Mode Mode // display metadata from query string
+
+	// package info
+	FSet       *token.FileSet       // nil if no package documentation
+	PDoc       *doc.Package         // nil if no package documentation
+	Examples   []*doc.Example       // nil if no example code
+	Bugs       []*doc.Note          // nil if no BUG comments
+	PAst       map[string]*ast.File // nil if no AST with package exports
+	IsMain     bool                 // true for package main
+	IsFiltered bool                 // true if results were filtered
+
+	// directory info
+	Dirs    *DirList // nil if no directory information
+	DirFlat bool     // if set, show directory in a flat (non-indented) manner
+}
+
+func (info *Page) IsEmpty() bool {
+	return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
+}
+
+type Mode uint
+
+const (
+	ModeAll     Mode = 1 << iota // do not filter exports
+	ModeFlat                     // show directory in a flat (non-indented) manner
+	ModeMethods                  // show all embedded methods
+	ModeSrc                      // show source code, do not extract documentation
+	ModeBuiltin                  // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
+)
+
+// modeNames defines names for each PageInfoMode flag.
+// The order here must match the order of the constants above.
+var modeNames = []string{
+	"all",
+	"flat",
+	"methods",
+	"src",
+}
+
+// generate a query string for persisting PageInfoMode between pages.
+func (m Mode) String() string {
+	s := ""
+	for i, name := range modeNames {
+		if m&(1<<i) != 0 && name != "" {
+			if s != "" {
+				s += ","
+			}
+			s += name
+		}
+	}
+	return s
+}
+
+// ParseMode computes the PageInfoMode flags by analyzing the request
+// URL form value "m". It is value is a comma-separated list of mode names (for example, "all,flat").
+func ParseMode(text string) Mode {
+	var mode Mode
+	for _, k := range strings.Split(text, ",") {
+		k = strings.TrimSpace(k)
+		for i, name := range modeNames {
+			if name == k {
+				mode |= 1 << i
+			}
+		}
+	}
+	return mode
+}
+
+// GetPageInfo returns the PageInfo for a package directory abspath. If the
+// parameter genAST is set, an AST containing only the package exports is
+// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
+// is extracted from the AST. If there is no corresponding package in the
+// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
+// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
+// set to the respective error but the error is not logged.
+func Doc(d *Docs, abspath, relpath string, mode Mode, goos, goarch string) *Page {
+	info := &Page{Dirname: abspath, Mode: mode}
+
+	// Restrict to the package files that would be used when building
+	// the package on this system.  This makes sure that if there are
+	// separate implementations for, say, Windows vs Unix, we don't
+	// jumble them all together.
+	// Note: If goos/goarch aren't set, the current binary's GOOS/GOARCH
+	// are used.
+	ctxt := build.Default
+	ctxt.IsAbsPath = path.IsAbs
+	ctxt.IsDir = func(path string) bool {
+		fi, err := fs.Stat(d.fs, toFS(filepath.ToSlash(path)))
+		return err == nil && fi.IsDir()
+	}
+	ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
+		f, err := fs.ReadDir(d.fs, toFS(filepath.ToSlash(dir)))
+		filtered := make([]os.FileInfo, 0, len(f))
+		for _, i := range f {
+			if mode&ModeAll != 0 || i.Name() != "internal" {
+				info, err := i.Info()
+				if err == nil {
+					filtered = append(filtered, info)
+				}
+			}
+		}
+		return filtered, err
+	}
+	ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
+		data, err := fs.ReadFile(d.fs, toFS(filepath.ToSlash(name)))
+		if err != nil {
+			return nil, err
+		}
+		return ioutil.NopCloser(bytes.NewReader(data)), nil
+	}
+
+	// Make the syscall/js package always visible by default.
+	// It defaults to the host's GOOS/GOARCH, and golang.org's
+	// linux/amd64 means the wasm syscall/js package was blank.
+	// And you can't run godoc on js/wasm anyway, so host defaults
+	// don't make sense here.
+	if goos == "" && goarch == "" && relpath == "syscall/js" {
+		goos, goarch = "js", "wasm"
+	}
+	if goos != "" {
+		ctxt.GOOS = goos
+	}
+	if goarch != "" {
+		ctxt.GOARCH = goarch
+	}
+
+	pkginfo, err := ctxt.ImportDir(abspath, 0)
+	// continue if there are no Go source files; we still want the directory info
+	if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
+		info.Err = err
+		return info
+	}
+
+	// collect package files
+	pkgname := pkginfo.Name
+	pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
+	if len(pkgfiles) == 0 {
+		// Commands written in C have no .go files in the build.
+		// Instead, documentation may be found in an ignored file.
+		// The file may be ignored via an explicit +build ignore
+		// constraint (recommended), or by defining the package
+		// documentation (historic).
+		pkgname = "main" // assume package main since pkginfo.Name == ""
+		pkgfiles = pkginfo.IgnoredGoFiles
+	}
+
+	// get package information, if any
+	if len(pkgfiles) > 0 {
+		// build package AST
+		fset := token.NewFileSet()
+		files, err := parseFiles(d.fs, fset, relpath, abspath, pkgfiles)
+		if err != nil {
+			info.Err = err
+			return info
+		}
+
+		// ignore any errors - they are due to unresolved identifiers
+		pkg, _ := ast.NewPackage(fset, files, simpleImporter, nil)
+
+		// extract package documentation
+		info.FSet = fset
+		if mode&ModeSrc == 0 {
+			// show extracted documentation
+			var m doc.Mode
+			if mode&ModeAll != 0 {
+				m |= doc.AllDecls
+			}
+			if mode&ModeMethods != 0 {
+				m |= doc.AllMethods
+			}
+			info.PDoc = doc.New(pkg, path.Clean(relpath), m) // no trailing '/' in importpath
+			if mode&ModeBuiltin != 0 {
+				for _, t := range info.PDoc.Types {
+					info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
+					info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
+					info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
+					t.Consts = nil
+					t.Vars = nil
+					t.Funcs = nil
+				}
+				// for now we cannot easily sort consts and vars since
+				// go/doc.Value doesn't export the order information
+				sort.Sort(funcsByName(info.PDoc.Funcs))
+			}
+
+			// collect examples
+			testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
+			files, err = parseFiles(d.fs, fset, relpath, abspath, testfiles)
+			if err != nil {
+				log.Println("parsing examples:", err)
+			}
+			info.Examples = collectExamples(pkg, files)
+			info.Bugs = info.PDoc.Notes["BUG"]
+		} else {
+			// show source code
+			// TODO(gri) Consider eliminating export filtering in this mode,
+			//           or perhaps eliminating the mode altogether.
+			if mode&ModeAll == 0 {
+				packageExports(fset, pkg)
+			}
+			info.PAst = files
+		}
+		info.IsMain = pkgname == "main"
+	}
+
+	info.Dirs = d.root.Lookup(abspath).List(func(path string) bool { return d.includePath(path, mode) })
+	info.DirFlat = mode&ModeFlat != 0
+
+	return info
+}
+
+func (d *Docs) includePath(path string, mode Mode) (r bool) {
+	// if the path includes 'internal', don't list unless we are in the NoFiltering mode.
+	if mode&ModeAll != 0 {
+		return true
+	}
+	if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
+		for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
+			if c == "internal" || c == "vendor" {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+// simpleImporter returns a (dummy) package object named
+// by the last path component of the provided package path
+// (as is the convention for packages). This is sufficient
+// to resolve package identifiers without doing an actual
+// import. It never returns an error.
+//
+func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
+	pkg := imports[path]
+	if pkg == nil {
+		// note that strings.LastIndex returns -1 if there is no "/"
+		pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
+		pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
+		imports[path] = pkg
+	}
+	return pkg, nil
+}
+
+// packageExports is a local implementation of ast.PackageExports
+// which correctly updates each package file's comment list.
+// (The ast.PackageExports signature is frozen, hence the local
+// implementation).
+//
+func packageExports(fset *token.FileSet, pkg *ast.Package) {
+	for _, src := range pkg.Files {
+		cmap := ast.NewCommentMap(fset, src, src.Comments)
+		ast.FileExports(src)
+		src.Comments = cmap.Filter(src).Comments()
+	}
+}
+
+type funcsByName []*doc.Func
+
+func (s funcsByName) Len() int { return len(s) }
+
+func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
+
+// collectExamples collects examples for pkg from testfiles.
+func collectExamples(pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
+	var files []*ast.File
+	for _, f := range testfiles {
+		files = append(files, f)
+	}
+
+	var examples []*doc.Example
+	globals := globalNames(pkg)
+	for _, e := range doc.Examples(files...) {
+		name := TrimExampleSuffix(e.Name)
+		if name == "" || globals[name] {
+			examples = append(examples, e)
+		}
+	}
+
+	return examples
+}
+
+// globalNames returns a set of the names declared by all package-level
+// declarations. Method names are returned in the form Receiver_Method.
+func globalNames(pkg *ast.Package) map[string]bool {
+	names := make(map[string]bool)
+	for _, file := range pkg.Files {
+		for _, decl := range file.Decls {
+			addNames(names, decl)
+		}
+	}
+	return names
+}
+
+// addNames adds the names declared by decl to the names set.
+// Method names are added in the form ReceiverTypeName_Method.
+func addNames(names map[string]bool, decl ast.Decl) {
+	switch d := decl.(type) {
+	case *ast.FuncDecl:
+		name := d.Name.Name
+		if d.Recv != nil {
+			var typeName string
+			switch r := d.Recv.List[0].Type.(type) {
+			case *ast.StarExpr:
+				typeName = r.X.(*ast.Ident).Name
+			case *ast.Ident:
+				typeName = r.Name
+			}
+			name = typeName + "_" + name
+		}
+		names[name] = true
+	case *ast.GenDecl:
+		for _, spec := range d.Specs {
+			switch s := spec.(type) {
+			case *ast.TypeSpec:
+				names[s.Name.Name] = true
+			case *ast.ValueSpec:
+				for _, id := range s.Names {
+					names[id.Name] = true
+				}
+			}
+		}
+	}
+}
+
+func SplitExampleName(s string) (name, suffix string) {
+	i := strings.LastIndex(s, "_")
+	if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
+		name = s[:i]
+		suffix = " (" + strings.Title(s[i+1:]) + ")"
+		return
+	}
+	name = s
+	return
+}
+
+// TrimExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
+// while keeping uppercase Braz in Foo_Braz.
+func TrimExampleSuffix(name string) string {
+	if i := strings.LastIndex(name, "_"); i != -1 {
+		if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
+			name = name[:i]
+		}
+	}
+	return name
+}
+
+func startsWithUppercase(s string) bool {
+	r, _ := utf8.DecodeRuneInString(s)
+	return unicode.IsUpper(r)
+}
diff --git a/internal/pkgdoc/doc_test.go b/internal/pkgdoc/doc_test.go
new file mode 100644
index 0000000..c82d042
--- /dev/null
+++ b/internal/pkgdoc/doc_test.go
@@ -0,0 +1,65 @@
+// Copyright 2018 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.
+
+//go:build go1.16
+// +build go1.16
+
+package pkgdoc
+
+import (
+	"testing"
+	"testing/fstest"
+)
+
+// TestIgnoredGoFiles tests the scenario where a folder has no .go or .c files,
+// but has an ignored go file.
+func TestIgnoredGoFiles(t *testing.T) {
+	packagePath := "github.com/package"
+	packageComment := "main is documented in an ignored .go file"
+
+	fs := fstest.MapFS{
+		"src/" + packagePath + "/ignored.go": {Data: []byte(`// +build ignore
+
+// ` + packageComment + `
+package main`)},
+	}
+	d := NewDocs(fs)
+	pInfo := Doc(d, "/src/"+packagePath, packagePath, ModeAll, "linux", "amd64")
+
+	if pInfo.PDoc == nil {
+		t.Error("pInfo.PDoc = nil; want non-nil.")
+	} else {
+		if got, want := pInfo.PDoc.Doc, packageComment+"\n"; got != want {
+			t.Errorf("pInfo.PDoc.Doc = %q; want %q.", got, want)
+		}
+		if got, want := pInfo.PDoc.Name, "main"; got != want {
+			t.Errorf("pInfo.PDoc.Name = %q; want %q.", got, want)
+		}
+		if got, want := pInfo.PDoc.ImportPath, packagePath; got != want {
+			t.Errorf("pInfo.PDoc.ImportPath = %q; want %q.", got, want)
+		}
+	}
+	if pInfo.FSet == nil {
+		t.Error("pInfo.FSet = nil; want non-nil.")
+	}
+}
+
+func TestIssue5247(t *testing.T) {
+	const packagePath = "example.com/p"
+	fs := fstest.MapFS{
+		"src/" + packagePath + "/p.go": {Data: []byte(`package p
+
+//line notgen.go:3
+// F doc //line 1 should appear
+// line 2 should appear
+func F()
+//line foo.go:100`)}, // No newline at end to check corner cases.
+	}
+
+	d := NewDocs(fs)
+	pInfo := Doc(d, "/src/"+packagePath, packagePath, 0, "linux", "amd64")
+	if got, want := pInfo.PDoc.Funcs[0].Doc, "F doc //line 1 should appear\nline 2 should appear\n"; got != want {
+		t.Errorf("pInfo.PDoc.Funcs[0].Doc = %q; want %q", got, want)
+	}
+}