// Copyright 2023 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 analyzerutil

import (
	"fmt"
	"go/parser"
	"go/token"
	"strings"
)

// MustExtractDoc is like [ExtractDoc] but it panics on error.
//
// To use, define a doc.go file such as:
//
//	// Package halting defines an analyzer of program termination.
//	//
//	// # Analyzer halting
//	//
//	// halting: reports whether execution will halt.
//	//
//	// The halting analyzer reports a diagnostic for functions
//	// that run forever. To suppress the diagnostics, try inserting
//	// a 'break' statement into each loop.
//	package halting
//
//	import _ "embed"
//
//	//go:embed doc.go
//	var doc string
//
// And declare your analyzer as:
//
//	var Analyzer = &analysis.Analyzer{
//		Name:             "halting",
//		Doc:              analyzerutil.MustExtractDoc(doc, "halting"),
//		...
//	}
func MustExtractDoc(content, name string) string {
	doc, err := ExtractDoc(content, name)
	if err != nil {
		panic(err)
	}
	return doc
}

// ExtractDoc extracts a section of a package doc comment from the
// provided contents of an analyzer package's doc.go file.
//
// A section is a portion of the comment between one heading and
// the next, using this form:
//
//	# Analyzer NAME
//
//	NAME: SUMMARY
//
//	Full description...
//
// where NAME matches the name argument, and SUMMARY is a brief
// verb-phrase that describes the analyzer. The following lines, up
// until the next heading or the end of the comment, contain the full
// description. ExtractDoc returns the portion following the colon,
// which is the form expected by Analyzer.Doc.
//
// Example:
//
//	# Analyzer printf
//
//	printf: checks consistency of calls to printf
//
//	The printf analyzer checks consistency of calls to printf.
//	Here is the complete description...
//
// This notation allows a single doc comment to provide documentation
// for multiple analyzers, each in its own section.
// The HTML anchors generated for each heading are predictable.
//
// It returns an error if the content was not a valid Go source file
// containing a package doc comment with a heading of the required
// form.
//
// This machinery enables the package documentation (typically
// accessible via the web at https://pkg.go.dev/) and the command
// documentation (typically printed to a terminal) to be derived from
// the same source and formatted appropriately.
func ExtractDoc(content, name string) (string, error) {
	if content == "" {
		return "", fmt.Errorf("empty Go source file")
	}
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "", content, parser.ParseComments|parser.PackageClauseOnly)
	if err != nil {
		return "", fmt.Errorf("not a Go source file")
	}
	if f.Doc == nil {
		return "", fmt.Errorf("Go source file has no package doc comment")
	}
	for section := range strings.SplitSeq(f.Doc.Text(), "\n# ") {
		if body := strings.TrimPrefix(section, "Analyzer "+name); body != section &&
			body != "" &&
			body[0] == '\r' || body[0] == '\n' {
			body = strings.TrimSpace(body)
			rest := strings.TrimPrefix(body, name+":")
			if rest == body {
				return "", fmt.Errorf("'Analyzer %s' heading not followed by '%s: summary...' line", name, name)
			}
			return strings.TrimSpace(rest), nil
		}
	}
	return "", fmt.Errorf("package doc comment contains no 'Analyzer %s' heading", name)
}
