Files
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

325 lines
8.7 KiB
Go

// Command gen-plugin-godocs generates docs/site/data/plugin-godocs.json, the data source consumed
// by the <PluginGoDocs /> and <PluginGoExample /> React components that render the server plugin
// SDK reference (docs/develop/integrate/reference/server/index.md).
//
// It reads the server/public/plugin package directly from this monorepo. It deliberately parses
// the package with go/parser + go/doc rather than type-checking it via golang.org/x/tools/go/packages,
// so it has no dependency on the Go toolchain version required by server/public/go.mod (the docs
// site's build environment may lag behind it).
package main
import (
"bytes"
"encoding/json"
"fmt"
"go/ast"
"go/doc"
"go/parser"
"go/printer"
"go/token"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
)
const pluginImportPath = "github.com/mattermost/mattermost/server/public/plugin"
type Field struct {
Names []string `json:"Names,omitempty"`
Type string
}
type MethodDocs struct {
Name string
Tags []string `json:"Tags,omitempty"`
HTML string
Parameters []*Field `json:"Parameters,omitempty"`
Results []*Field `json:"Results,omitempty"`
}
type InterfaceDocs struct {
HTML string
Tags []string `json:"Tags,omitempty"`
Methods []*MethodDocs
}
type ExampleDocs struct {
HTML string
Code string
}
type Docs struct {
HTML string
API InterfaceDocs
Hooks InterfaceDocs
Helpers InterfaceDocs
Examples map[string]*ExampleDocs
}
func pluginPackageDir() string {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
log.Fatal("unable to determine source file location")
}
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "..", ".."))
return filepath.Join(repoRoot, "server", "public", "plugin")
}
func docHTML(text string) string {
buf := &bytes.Buffer{}
doc.ToHTML(buf, text, nil)
return buf.String()
}
func removeDuplicates(items []string) []string {
seen := make(map[string]bool, len(items))
out := make([]string, 0, len(items))
for _, item := range items {
if !seen[item] {
seen[item] = true
out = append(out, item)
}
}
return out
}
var tagRegexp = regexp.MustCompile(`@tag\s+(\w+)\s*`)
func tags(text string) []string {
submatches := tagRegexp.FindAllStringSubmatch(text, -1)
out := make([]string, len(submatches))
for i, submatch := range submatches {
out[i] = submatch[1]
}
return removeDuplicates(out)
}
// builtinTypes is the set of predeclared Go types documented at https://pkg.go.dev/builtin.
var builtinTypes = map[string]bool{
"bool": true, "byte": true, "complex128": true, "complex64": true, "error": true,
"float32": true, "float64": true, "int": true, "int16": true, "int32": true, "int64": true,
"int8": true, "rune": true, "string": true, "uint": true, "uint16": true, "uint32": true,
"uint64": true, "uint8": true, "uintptr": true, "any": true,
}
// importAliases maps the local identifier a file uses for an imported package (its alias, or the
// last path segment when unaliased) to that package's full import path.
func importAliases(file *ast.File) map[string]string {
aliases := make(map[string]string)
for _, imp := range file.Imports {
path := strings.Trim(imp.Path.Value, `"`)
alias := path[strings.LastIndex(path, "/")+1:]
if imp.Name != nil {
alias = imp.Name.Name
}
aliases[alias] = path
}
return aliases
}
// typeString renders a field's type expression as a dotted, fully-qualified string, e.g.
// "[]*github.com/mattermost/mattermost/server/public/model.Manifest", the same shape the old
// go/types-based generator produced. Types local to the plugin package are qualified with
// pluginImportPath so the renderer can link to them the same way it links to imported types.
func typeString(expr ast.Expr, aliases map[string]string) string {
switch x := expr.(type) {
case *ast.StarExpr:
return "*" + typeString(x.X, aliases)
case *ast.ArrayType:
return "[]" + typeString(x.Elt, aliases)
case *ast.Ellipsis:
return "[]" + typeString(x.Elt, aliases)
case *ast.MapType:
return "map[" + typeString(x.Key, aliases) + "]" + typeString(x.Value, aliases)
case *ast.ChanType:
return "chan " + typeString(x.Value, aliases)
case *ast.InterfaceType:
if x.Methods == nil || len(x.Methods.List) == 0 {
return "interface{}"
}
return "interface{ ... }"
case *ast.SelectorExpr:
pkgIdent, ok := x.X.(*ast.Ident)
if !ok {
return x.Sel.Name
}
if path, ok := aliases[pkgIdent.Name]; ok {
return path + "." + x.Sel.Name
}
return pkgIdent.Name + "." + x.Sel.Name
case *ast.Ident:
if builtinTypes[x.Name] || x.Name == "byte" || x.Name == "rune" {
return x.Name
}
// A bare identifier that isn't a builtin must refer to a type declared in this same
// package (plugin), since any imported type is always qualified with a selector.
return pluginImportPath + "." + x.Name
default:
buf := &bytes.Buffer{}
_ = printer.Fprint(buf, token.NewFileSet(), expr)
return buf.String()
}
}
func fields(list *ast.FieldList, aliases map[string]string) (out []*Field) {
if list == nil {
return nil
}
for _, x := range list.List {
field := &Field{}
for _, name := range x.Names {
field.Names = append(field.Names, name.Name)
}
t := typeString(x.Type, aliases)
if _, ok := x.Type.(*ast.Ellipsis); ok {
t = "..." + strings.TrimPrefix(t, "[]")
}
field.Type = t
out = append(out, field)
}
return out
}
func fileForPos(fset *token.FileSet, files map[string]*ast.File, pos token.Pos) *ast.File {
name := fset.Position(pos).Filename
return files[name]
}
func generateDocs() (*Docs, error) {
pluginDir := pluginPackageDir()
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, pluginDir, nil, parser.ParseComments)
if err != nil {
return nil, err
}
docs := &Docs{
Examples: make(map[string]*ExampleDocs),
}
// Files keyed by absolute path, across both the "plugin" and "plugin_test" (external test)
// packages, so we can recover which file a given interface method came from and resolve its
// imports, and so examples defined in either package are picked up.
filesByPath := make(map[string]*ast.File)
var allFiles []*ast.File
for _, pkg := range pkgs {
for path, file := range pkg.Files {
filesByPath[path] = file
allFiles = append(allFiles, file)
}
}
for _, example := range doc.Examples(allFiles...) {
// Play is a synthesized, standalone runnable program and is preferred when available;
// it's nil when go/doc can't build one (e.g. the example can't be wrapped as a whole
// program), in which case Code — the example function's body, always non-nil — is used
// instead.
var node ast.Node = example.Play
if example.Play == nil {
node = example.Code
}
buf := &bytes.Buffer{}
if err := printer.Fprint(buf, fset, node); err != nil {
return nil, fmt.Errorf("failed to print example %q: %w", example.Name, err)
}
docs.Examples[example.Name] = &ExampleDocs{
HTML: docHTML(example.Doc),
Code: buf.String(),
}
}
pluginPkg, ok := pkgs["plugin"]
if !ok {
return nil, os.ErrNotExist
}
godocs := doc.New(pluginPkg, pluginImportPath, doc.Mode(0))
if godocs.Name == "plugin" && godocs.Doc != "" {
docs.HTML = docHTML(godocs.Doc)
}
for _, t := range godocs.Types {
var interfaceDocs *InterfaceDocs
switch t.Name {
case "API":
interfaceDocs = &docs.API
case "Hooks":
interfaceDocs = &docs.Hooks
case "Helpers":
interfaceDocs = &docs.Helpers
default:
continue
}
if t.Doc != "" {
interfaceDocs.HTML = docHTML(t.Doc)
}
for _, spec := range t.Decl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
iface, ok := typeSpec.Type.(*ast.InterfaceType)
if !ok {
continue
}
file := fileForPos(fset, filesByPath, typeSpec.Pos())
var aliases map[string]string
if file != nil {
aliases = importAliases(file)
}
allTags := make([]string, 0)
for _, method := range iface.Methods.List {
funcType, ok := method.Type.(*ast.FuncType)
if !ok || len(method.Names) == 0 {
continue
}
methodDocs := &MethodDocs{
Name: method.Names[0].Name,
Tags: tags(method.Doc.Text()),
HTML: docHTML(method.Doc.Text()),
Parameters: fields(funcType.Params, aliases),
Results: fields(funcType.Results, aliases),
}
interfaceDocs.Methods = append(interfaceDocs.Methods, methodDocs)
allTags = append(allTags, methodDocs.Tags...)
}
allTags = removeDuplicates(allTags)
sort.Strings(allTags)
interfaceDocs.Tags = allTags
}
}
return docs, nil
}
func main() {
docs, err := generateDocs()
if err != nil {
log.Fatal(err)
}
b, err := json.MarshalIndent(docs, "", " ")
if err != nil {
log.Fatal(err)
}
if _, err := os.Stdout.Write(b); err != nil {
log.Fatal(err)
}
if _, err := os.Stdout.Write([]byte("\n")); err != nil {
log.Fatal(err)
}
}