snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
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 Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package openApiSync
import (
"bytes"
"fmt"
"go/ast"
"go/printer"
"go/token"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"github.com/pb33f/libopenapi"
v3high "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pkg/errors"
"github.com/sajari/fuzzy"
"golang.org/x/tools/go/analysis"
)
var (
Analyzer = &analysis.Analyzer{
Name: "openApiSync",
Doc: "check for inconsistencies between OpenAPI spec and the source code",
Run: run,
}
specFile string
groupSplitRegexp = regexp.MustCompile(`{([a-z_]*):([a-z_]*)\|([a-z_]*)}`)
// Excludes | so group-alternation patterns like {type:a|b} are left intact for splitHandlerByGroup.
pathParamConstraintRegex = regexp.MustCompile(`\{([^:}]+):[^|}]+\}`)
openAPIParamRegex = regexp.MustCompile(`\{[^}]+\}`)
IgnoredCases = []string{"{websocket}", "api/v4/remotecluster"}
)
func init() {
Analyzer.Flags.StringVar(&specFile, "spec", "", "Path to the OpenAPI 3 YAML spec file")
}
// formatNode converts AST node to string
func formatNode(fset *token.FileSet, node any) string {
var typeNameBuf bytes.Buffer
_ = printer.Fprint(&typeNameBuf, fset, node)
return typeNameBuf.String()
}
// stringInSlice checks presence of a string in a slice
func stringInSlice(str string, slice []string, partial bool) bool {
for _, s := range slice {
if s == str {
return true
}
if partial && strings.Contains(str, s) {
return true
}
}
return false
}
// cleanRegexp strips regex constraints from Go router path parameters so they
// can be matched against OpenAPI path templates.
// e.g., "/users/{user_id:[0-9]+}" → "/users/{user_id}"
func cleanRegexp(s string) string {
return pathParamConstraintRegex.ReplaceAllString(s, "{$1}")
}
// matchesTemplate returns true if the OpenAPI specPath template matches handlerPath.
// Spec path parameters (e.g., {foo}) match any single non-slash segment, mirroring
// the behavior of kin-openapi's Paths.Find().
func matchesTemplate(specPath, handlerPath string) bool {
specParts := strings.Split(strings.Trim(specPath, "/"), "/")
handlerParts := strings.Split(strings.Trim(handlerPath, "/"), "/")
if len(specParts) != len(handlerParts) {
return false
}
for i, specPart := range specParts {
if openAPIParamRegex.MatchString(specPart) {
continue
}
if specPart != handlerParts[i] {
return false
}
}
return true
}
// findPathItem returns the path item matching handlerPath in the spec.
// It first tries exact key lookup, then falls back to template matching.
func findPathItem(paths *v3high.Paths, handlerPath string) *v3high.PathItem {
if item := paths.PathItems.GetOrZero(handlerPath); item != nil {
return item
}
for specPath, item := range paths.PathItems.FromOldest() {
if matchesTemplate(specPath, handlerPath) {
return item
}
}
return nil
}
// splitHandlerByGroup checks if URL path regexp contains named groups, and splits them in separate paths to be compatible with OpenAPI paths
func splitHandlerByGroup(str string) []string {
matches := groupSplitRegexp.FindAllStringSubmatch(str, -1)
if len(matches) != 1 {
return []string{str}
}
group := matches[0][0]
part1 := matches[0][2]
part2 := matches[0][3]
return []string{strings.Replace(str, group, part1, 1), strings.Replace(str, group, part2, 1)}
}
// getOperation returns the operation for the given HTTP method on a path item, or nil if not defined.
func getOperation(pathItem *v3high.PathItem, method string) *v3high.Operation {
switch strings.ToUpper(method) {
case http.MethodGet:
return pathItem.Get
case http.MethodPost:
return pathItem.Post
case http.MethodPut:
return pathItem.Put
case http.MethodDelete:
return pathItem.Delete
case http.MethodPatch:
return pathItem.Patch
case http.MethodHead:
return pathItem.Head
case http.MethodOptions:
return pathItem.Options
case http.MethodTrace:
return pathItem.Trace
}
return nil
}
// processRouterInit checks that all Init functions defined in `names` are properly documented
func processRouterInit(pass *analysis.Pass, names []string, routerPrefixes map[string]string, paths *v3high.Paths, cm *fuzzy.Model) {
for _, file := range pass.Files {
ast.Inspect(file, func(n ast.Node) bool {
decl, ok := n.(*ast.FuncDecl)
if !ok || !stringInSlice(decl.Name.Name, names, false) {
return true
}
for _, stmt := range decl.Body.List {
expr, ok := stmt.(*ast.ExprStmt)
if !ok {
continue
}
aexpr, ok := expr.X.(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.CallExpr)
if !ok || len(aexpr.Args) != 2 {
continue
}
prefix, _ := strconv.Unquote(aexpr.Args[0].(*ast.BasicLit).Value)
method := getMethodFromExpr(expr)
name := aexpr.Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr).Sel.Name
handler := cleanRegexp(routerPrefixes[name]) + cleanRegexp(prefix)
if stringInSlice(handler, IgnoredCases, true) { // ignore special cases
continue
}
if !strings.HasPrefix(handler, "/") {
handler = "/" + handler
}
for _, h := range splitHandlerByGroup(handler) {
pathItem := findPathItem(paths, h)
if pathItem == nil {
suffix := ""
if suggestions := cm.Suggestions(h, false); len(suggestions) > 0 {
suffix = fmt.Sprintf(" (maybe you meant: %v)", suggestions)
}
pass.Reportf(aexpr.Pos(), "Cannot find %v method: %v in OpenAPI 3 spec.%s", h, method, suffix)
} else if getOperation(pathItem, method) == nil {
pass.Reportf(aexpr.Pos(), "Handler %v is defined with method %s, but it's not in the spec", h, method)
}
}
}
return true
})
}
}
// parseRoutesStruct scans Routes struct for mux.Router fields and scans their comments
func parseRoutesStruct(pass *analysis.Pass, decl *ast.GenDecl, routerPrefixes map[string]string) {
spec, ok := decl.Specs[0].(*ast.TypeSpec)
if !ok || spec.Name.String() != "Routes" {
return
}
for _, f := range spec.Type.(*ast.StructType).Fields.List {
typeName := formatNode(pass.Fset, f.Type)
if typeName != "*mux.Router" {
continue
}
routerName := f.Names[0].Name
if routerName == "ApiRoot" || routerName == "Root" {
continue
}
if f.Comment != nil && len(f.Comment.List) > 0 {
comment := f.Comment.List[0].Text
if strings.HasPrefix(comment, "// '") && strings.HasSuffix(comment, "'") {
routerPrefixes[routerName] = comment[4 : len(comment)-1]
} else {
pass.Reportf(f.Comment.List[0].Pos(), "Comment for field %s is not formatted correctly\n", routerName)
}
} else {
pass.Reportf(f.Comment.Pos(), "Router field %s in Router struct is not commented properly\n", routerName)
}
}
}
// parseInitFunction checks a specific Init function for proper documentation of registered API handlers
func parseInitFunction(pass *analysis.Pass, decl *ast.FuncDecl, routerPrefixes map[string]string, initFunctions []string) []string {
for _, stmt := range decl.Body.List {
switch node := stmt.(type) {
case *ast.ExprStmt:
call, ok := node.X.(*ast.CallExpr)
if !ok {
continue
}
selector, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
ident, ok := selector.X.(*ast.Ident)
if ok && ident.Name == "api" {
initFunctions = append(initFunctions, selector.Sel.Name)
}
case *ast.AssignStmt:
if len(node.Lhs) != 1 || !strings.HasPrefix(formatNode(pass.Fset, node.Lhs[0]), "api.BaseRoutes") {
continue
}
subRouterName := formatNode(pass.Fset, node.Lhs[0])[15:]
if subRouterName == "APIRoot" || subRouterName == "APIRoot5" || subRouterName == "Root" {
continue
}
rhs := formatNode(pass.Fset, node.Rhs[0])[15:]
router := rhs[:strings.Index(rhs, ".")]
path := rhs[strings.Index(rhs, ".")+13 : strings.LastIndex(rhs, ".")-2]
prefix := ""
switch router {
case "APIRoot":
prefix = "api/v4"
case "Root":
prefix = ""
default:
if s, ok := routerPrefixes[router]; ok {
prefix = s
} else {
pass.Reportf(node.Rhs[0].Pos(), "cannot find prefix for %s\n", router)
}
}
s := fmt.Sprintf("%v%v", prefix, path)
s2 := routerPrefixes[subRouterName]
if s2 != s {
pass.Reportf(node.Rhs[0].Pos(), "PathPrefix doesn't match field comment for field '%s': '%s' vs '%s'\n", subRouterName, s, s2)
}
}
}
return initFunctions
}
func validateComments(pass *analysis.Pass) ([]string, map[string]string) {
initFunctions := []string{}
routerPrefixes := map[string]string{}
for _, file := range pass.Files {
ast.Inspect(file, func(n ast.Node) bool {
if decl, ok := n.(*ast.GenDecl); ok && len(decl.Specs) == 1 {
parseRoutesStruct(pass, decl, routerPrefixes)
} else if decl, ok := n.(*ast.FuncDecl); ok && decl.Name.Name == "Init" {
initFunctions = parseInitFunction(pass, decl, routerPrefixes, initFunctions)
}
return true
})
}
return initFunctions, routerPrefixes
}
func run(pass *analysis.Pass) (any, error) {
if specFile == "" {
return nil, errors.New("Please supply a path to OpenAPI spec yaml file via -openApiSync.spec")
}
if _, err := os.Stat(specFile); err != nil {
return nil, errors.Wrapf(err, "spec file does not exist")
}
data, err := os.ReadFile(specFile)
if err != nil {
return nil, errors.Wrapf(err, "Unable to read spec file")
}
doc, err := libopenapi.NewDocument(data)
if err != nil {
return nil, errors.Wrapf(err, "Unable to parse spec file. Expected OpenAPI3 format.")
}
model, err := doc.BuildV3Model()
if err != nil {
return nil, errors.Wrapf(err, "Unable to build OpenAPI3 model")
}
initFunctions, routerPrefixes := validateComments(pass)
var swaggerPaths []string
for p := range model.Model.Paths.PathItems.KeysFromOldest() {
swaggerPaths = append(swaggerPaths, p)
}
fuzzyModel := fuzzy.NewModel()
fuzzyModel.Train(swaggerPaths)
processRouterInit(pass, initFunctions, routerPrefixes, model.Model.Paths, fuzzyModel)
return nil, nil
}
func getMethodFromExpr(expr *ast.ExprStmt) string {
var method string
methodArg := expr.X.(*ast.CallExpr).Args[0]
if methodSelectorExpr, ok := methodArg.(*ast.SelectorExpr); ok {
if methodSelectorExpr.X.(*ast.Ident).Name == "http" {
switch methodSelectorExpr.Sel.Name {
case "MethodGet":
method = http.MethodGet
case "MethodHead":
method = http.MethodHead
case "MethodPost":
method = http.MethodPost
case "MethodPut":
method = http.MethodPut
case "MethodPatch":
method = http.MethodPatch
case "MethodDelete":
method = http.MethodDelete
case "MethodConnect":
method = http.MethodConnect
case "MethodOptions":
method = http.MethodOptions
case "MethodTrace":
method = http.MethodTrace
}
}
} else if methodBasicLit, ok := methodArg.(*ast.BasicLit); ok {
method, _ = strconv.Unquote(methodBasicLit.Value)
}
return method
}

View File

@@ -0,0 +1,148 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package openApiSync
import (
"net/http"
"testing"
v3high "github.com/pb33f/libopenapi/datamodel/high/v3"
"golang.org/x/tools/go/analysis/analysistest"
)
func Test(t *testing.T) {
testdata := analysistest.TestData()
specFile = analysistest.TestData() + "/spec.yaml"
analysistest.Run(t, testdata, Analyzer, "api")
}
func TestCleanRegexp(t *testing.T) {
tests := []struct {
input string
want string
}{
{"/api/v4/users/{user_id:[0-9]+}", "/api/v4/users/{user_id}"},
{"/api/v4/users/{username:[A-Za-z0-9\\_\\-\\.]+}", "/api/v4/users/{username}"},
{"/api/v4/jobs/type/{job_type:[A-Za-z0-9_-]+}", "/api/v4/jobs/type/{job_type}"},
// Literal-value constraints (e.g., {websocket:websocket}) are stripped like any other.
{"/api/v4/{websocket:websocket}", "/api/v4/{websocket}"},
// Group-alternation patterns must be left intact for splitHandlerByGroup.
{"/api/v4/groups/{group_id}/{syncable_type:teams|channels}/{syncable_id}/link", "/api/v4/groups/{group_id}/{syncable_type:teams|channels}/{syncable_id}/link"},
// No constraint — unchanged.
{"/api/v4/users/{user_id}/posts", "/api/v4/users/{user_id}/posts"},
// Multiple constrained params in one path.
{"/api/v4/users/{user_id:[0-9]+}/posts/{post_id:[0-9]+}", "/api/v4/users/{user_id}/posts/{post_id}"},
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
if got := cleanRegexp(tc.input); got != tc.want {
t.Errorf("cleanRegexp(%q) = %q, want %q", tc.input, got, tc.want)
}
})
}
}
func TestMatchesTemplate(t *testing.T) {
tests := []struct {
specPath string
handlerPath string
want bool
}{
// Exact match.
{
"/api/v4/groups/{group_id}/teams/{syncable_id}/link",
"/api/v4/groups/{group_id}/teams/{syncable_id}/link",
true,
},
// Spec param matches handler literal segment.
{
"/api/v4/groups/{group_id}/{syncable_type}/{syncable_id}/link",
"/api/v4/groups/{group_id}/channels/{syncable_id}/link",
true,
},
// Spec literal does not match a different handler literal.
{
"/api/v4/groups/{group_id}/teams/{syncable_id}/link",
"/api/v4/groups/{group_id}/channels/{syncable_id}/link",
false,
},
// Spec param matches handler param name.
{
"/api/v4/users/{user_id}",
"/api/v4/users/{user_id}",
true,
},
// Spec param matches handler literal "me".
{
"/api/v4/users/{user_id}",
"/api/v4/users/me",
true,
},
// Different path lengths.
{
"/api/v4/users/{user_id}/posts",
"/api/v4/users/{user_id}",
false,
},
// Completely different paths.
{
"/api/v4/teams/{team_id}",
"/api/v4/users/{user_id}",
false,
},
}
for _, tc := range tests {
t.Run(tc.specPath+"~"+tc.handlerPath, func(t *testing.T) {
if got := matchesTemplate(tc.specPath, tc.handlerPath); got != tc.want {
t.Errorf("matchesTemplate(%q, %q) = %v, want %v", tc.specPath, tc.handlerPath, got, tc.want)
}
})
}
}
func TestGetOperation(t *testing.T) {
get := &v3high.Operation{}
post := &v3high.Operation{}
put := &v3high.Operation{}
patch := &v3high.Operation{}
del := &v3high.Operation{}
head := &v3high.Operation{}
opts := &v3high.Operation{}
trace := &v3high.Operation{}
pathItem := &v3high.PathItem{
Get: get,
Post: post,
Put: put,
Patch: patch,
Delete: del,
Head: head,
Options: opts,
Trace: trace,
}
tests := []struct {
method string
want *v3high.Operation
}{
{http.MethodGet, get},
{http.MethodPost, post},
{http.MethodPut, put},
{http.MethodPatch, patch},
{http.MethodDelete, del},
{http.MethodHead, head},
{http.MethodOptions, opts},
{http.MethodTrace, trace},
{"get", get}, // case-insensitive
{"post", post}, // case-insensitive
{"UNKNOWN", nil},
}
for _, tc := range tests {
t.Run(tc.method, func(t *testing.T) {
if got := getOperation(pathItem, tc.method); got != tc.want {
t.Errorf("getOperation(pathItem, %q) = %v, want %v", tc.method, got, tc.want)
}
})
}
}

View File

@@ -0,0 +1,17 @@
openapi: "3.0.0"
info:
title: Test API
version: "1.0.0"
paths:
/api/v4/users/ids:
post:
summary: Get users by IDs
responses:
"200":
description: OK
/api/v4/groups/{group_id}/teams/{syncable_id}/link:
post:
summary: Link a group syncable
responses:
"200":
description: OK

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"net/http"
"github.com/gorilla/mux"
)
type Routes struct {
Root *mux.Router // ''
APIRoot *mux.Router // 'api/v4'
Users *mux.Router // 'api/v4/userzs'
Groups *mux.Router // 'api/v4/groups'
}
type API struct {
BaseRoutes *Routes
}
func (*API) ApiSessionRequired(h func(*context.Context, http.ResponseWriter, *http.Request)) http.Handler {
return nil
}
func Init(root *mux.Router) *API {
api := &API{
BaseRoutes: &Routes{},
}
api.BaseRoutes.Root = root
api.BaseRoutes.APIRoot = root.PathPrefix("api/v4").Subrouter()
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter() // want "PathPrefix doesn't match field comment for field 'Users': 'api/v4/users' vs 'api/v4/userzs'"
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/gruops").Subrouter() // want "PathPrefix doesn't match field comment for field 'Groups': 'api/v4/gruops' vs 'api/v4/groups'"
api.InitUsers()
return api
}
func _() {
_ = Init(nil)
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"net/http"
)
func (a *API) InitUsers() {
a.BaseRoutes.Users.Handle("/ids", a.ApiSessionRequired(getUsersByIds)).Methods(http.MethodPost) // want `Cannot find /api/v4/userzs/ids method: POST in OpenAPI 3 spec. \(maybe you meant: \[/api/v4/users/ids\]\)`
a.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channelz}/{syncable_id:[A-Za-z0-9]+}/link", a.ApiSessionRequired(getUsersByIds)).Methods("POST") // want `Cannot find /api/v4/groups/{group_id}/channelz/{syncable_id}/link method: POST in OpenAPI 3 spec.`
}
func getUsersByIds(c *context.Context, w http.ResponseWriter, r *http.Request) {
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mux
import "net/http"
type Router struct {
}
func (*Router) PathPrefix(_ string) *Router {
return &Router{}
}
func (*Router) Subrouter() *Router {
return &Router{}
}
func (*Router) Handle(_ string, h http.Handler) *Router {
return &Router{}
}
func (*Router) Methods(_ ...string) *Router {
return &Router{}
}