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

279 lines
7.7 KiB
Go

// Command gen-plugin-manifest-docs generates docs/site/data/plugin-manifest-docs.json, the data
// source consumed by the <PluginManifestDocs /> component that renders the plugin manifest
// reference (docs/develop/integrate/plugins/manifest-reference.md).
//
// It reads server/public/model directly from this monorepo, walking the AST with go/parser +
// go/doc instead of type-checking with golang.org/x/tools/go/packages (see gen-plugin-godocs for
// why: it avoids requiring a Go toolchain matching server/public/go.mod's declared version).
// Because of that, type resolution only follows types declared inside the model package's own
// source (which is all model.Manifest ever references) — it doesn't resolve identifiers to types
// from other packages.
package main
import (
"bytes"
"encoding/json"
"go/ast"
"go/doc"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
)
type SchemaType string
const (
Array SchemaType = "array"
Bool SchemaType = "bool"
Dict SchemaType = "dict"
Number SchemaType = "number"
Object SchemaType = "object"
String SchemaType = "string"
Interface SchemaType = "interface"
)
const modelImportPath = "github.com/mattermost/mattermost/server/public/model"
type ObjectProperty struct {
Name string
DocHTML string `json:"DocHTML,omitempty"`
Schema *TypeDocs `json:"Schema,omitempty"`
}
type TypeDocs struct {
Type SchemaType
DocHTML string `json:"DocHTML,omitempty"`
ObjectProperties []*ObjectProperty `json:"ObjectProperties,omitempty"`
ValueSchema *TypeDocs `json:"ValueSchema,omitempty"`
}
type Docs struct {
Schema *TypeDocs
}
var basicTypeKinds = map[string]SchemaType{
"string": String,
"bool": Bool,
"byte": Number,
"rune": Number,
"int": Number, "int8": Number, "int16": Number, "int32": Number, "int64": Number,
"uint": Number, "uint8": Number, "uint16": Number, "uint32": Number, "uint64": Number,
"float32": Number, "float64": Number,
}
// fset and filesByPath let namedTypeDocs recover which file (and therefore which imports) a given
// model package type was declared in, so SelectorExpr fields can be resolved against the model
// package's imports rather than by identifier name alone. Populated once in generateDocs.
var (
fset *token.FileSet
filesByPath map[string]*ast.File
)
// 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
}
func importsForPos(pos token.Pos) map[string]string {
file := filesByPath[fset.Position(pos).Filename]
if file == nil {
return nil
}
return importAliases(file)
}
func modelPackageDir() 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", "model")
}
func docHTML(text string) string {
buf := &bytes.Buffer{}
doc.ToHTML(buf, text, nil)
return buf.String()
}
func typeSpecOf(t *doc.Type) *ast.TypeSpec {
for _, spec := range t.Decl.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
return typeSpec
}
}
return nil
}
func namedTypeDocs(t *doc.Type, typesByName map[string]*doc.Type) *TypeDocs {
spec := typeSpecOf(t)
if spec == nil {
return nil
}
ret := exprTypeDocs(spec.Type, typesByName, importsForPos(spec.Pos()))
if ret != nil {
ret.DocHTML = docHTML(t.Doc)
}
return ret
}
// jsonFieldName returns the field's JSON key, mirroring encoding/json's own rules: unexported
// fields and those tagged `json:"-"` are never included; otherwise the tag's name component wins
// if present (including when a comma-only tag like `json:",omitempty"` leaves it empty, in which
// case the Go field name is used, same as an absent tag).
func jsonFieldName(field *ast.Field) string {
if len(field.Names) == 0 {
// Anonymous (embedded) fields aren't handled by this generator — none exist in the
// reachable model.Manifest schema.
return ""
}
fieldName := field.Names[0].Name
if !ast.IsExported(fieldName) {
return ""
}
if field.Tag == nil {
return fieldName
}
tagValue := strings.Trim(field.Tag.Value, "`")
jsonTag := reflect.StructTag(tagValue).Get("json")
if jsonTag == "-" {
return ""
}
name := strings.SplitN(jsonTag, ",", 2)[0]
if name == "" {
return fieldName
}
return name
}
func exprTypeDocs(expr ast.Expr, typesByName map[string]*doc.Type, imports map[string]string) *TypeDocs {
switch x := expr.(type) {
case *ast.StarExpr:
return exprTypeDocs(x.X, typesByName, imports)
case *ast.ArrayType:
return &TypeDocs{
Type: Array,
ValueSchema: exprTypeDocs(x.Elt, typesByName, imports),
}
case *ast.MapType:
return &TypeDocs{
Type: Dict,
ValueSchema: exprTypeDocs(x.Value, typesByName, imports),
}
case *ast.StructType:
ret := &TypeDocs{Type: Object}
for _, field := range x.Fields.List {
name := jsonFieldName(field)
if name == "" {
continue
}
ret.ObjectProperties = append(ret.ObjectProperties, &ObjectProperty{
Name: name,
DocHTML: docHTML(field.Doc.Text()),
Schema: exprTypeDocs(field.Type, typesByName, imports),
})
}
return ret
case *ast.SelectorExpr:
pkgIdent, ok := x.X.(*ast.Ident)
if !ok || imports[pkgIdent.Name] != modelImportPath {
log.Printf("unrecognized qualified type %v (not a reference to the model package)", x)
return nil
}
// A qualified reference to the model package's own import path (i.e. a self-import
// alias) still names a type declared in this same package, so resolve it the same way
// an unqualified identifier would be.
return exprTypeDocs(x.Sel, typesByName, imports)
case *ast.InterfaceType:
return &TypeDocs{Type: Interface}
case *ast.Ident:
if x.Name == "any" {
return &TypeDocs{Type: Interface}
}
if kind, ok := basicTypeKinds[x.Name]; ok {
return &TypeDocs{Type: kind}
}
if t, ok := typesByName[x.Name]; ok {
return namedTypeDocs(t, typesByName)
}
log.Printf("unrecognized identifier %q (not a builtin or a model package type)", x.Name)
return nil
}
log.Printf("unrecognized ast.Expr: %T", expr)
return nil
}
func generateDocs() (*Docs, error) {
modelDir := modelPackageDir()
fset = token.NewFileSet()
notTest := func(info os.FileInfo) bool { return !strings.HasSuffix(info.Name(), "_test.go") }
pkgs, err := parser.ParseDir(fset, modelDir, notTest, parser.ParseComments)
if err != nil {
return nil, err
}
modelPkg, ok := pkgs["model"]
if !ok {
return nil, os.ErrNotExist
}
filesByPath = make(map[string]*ast.File, len(modelPkg.Files))
for path, file := range modelPkg.Files {
filesByPath[path] = file
}
godocs := doc.New(modelPkg, "github.com/mattermost/mattermost/server/public/model", doc.Mode(0))
typesByName := make(map[string]*doc.Type, len(godocs.Types))
for _, t := range godocs.Types {
typesByName[t.Name] = t
}
manifestType, ok := typesByName["Manifest"]
if !ok {
return nil, os.ErrNotExist
}
return &Docs{
Schema: namedTypeDocs(manifestType, typesByName),
}, 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)
}
}