Inline mattermost-govet into the monorepo (#35869)

* inline mattermost-govet

* fix style issues

* simplify the openApiSync spec test

* README.md tweaks

* fix missing licenses

* simplify README.md

* trigger server-ci on tools/mattermost-govet/**

* Apply https://github.com/mattermost/mattermost-govet/commit/470cf78253f0aa85a4f4b5b50002682bf823e729

---------

Co-authored-by: Mattermost Build <[email protected]>
This commit is contained in:
Jesse Hallam
2026-04-01 13:24:22 +00:00
committed by GitHub
co-authored by Mattermost Build
parent 083681f8ca
commit a87917360a
150 changed files with 5939 additions and 2 deletions
+1
View File
@@ -17,6 +17,7 @@ on:
- ".github/workflows/mmctl-test-template.yml"
- "!server/build/Dockerfile.buildenv"
- "!server/build/Dockerfile.buildenv-fips"
- "tools/mattermost-govet/**"
- "!server/**/*.md"
- "!server/NOTICE.txt"
- "!server/CHANGELOG.md"
+47
View File
@@ -0,0 +1,47 @@
name: Tools CI
on:
push:
branches:
- master
- release-*
pull_request:
paths:
- "tools/mattermost-govet/**"
- ".github/workflows/tools-ci.yml"
concurrency:
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
check-style:
name: check-style (mattermost-govet)
runs-on: ubuntu-22.04
defaults:
run:
working-directory: tools/mattermost-govet
steps:
- name: Checkout mattermost project
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version-file: tools/mattermost-govet/go.mod
- name: Run check-style
run: make check-style
test:
name: Test (mattermost-govet)
runs-on: ubuntu-22.04
defaults:
run:
working-directory: tools/mattermost-govet
steps:
- name: Checkout mattermost project
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version-file: tools/mattermost-govet/go.mod
- name: Run tests
run: make test
+2 -2
View File
@@ -805,7 +805,7 @@ ifeq ($(BUILD_ENTERPRISE_READY),true)
endif
vet: setup-go-work ## Run mattermost go vet specific checks
$(GO) install github.com/mattermost/mattermost-govet/v2@b9756c1328ecab43db9b0a892eaa6d4d0a7227b1
cd ../tools/mattermost-govet && $(GO) install .
$(GO) vet -vettool=$(GOBIN)/mattermost-govet \
-structuredLogging \
-inconsistentReceiverName \
@@ -831,7 +831,7 @@ vet-api: export GO := $(GO)
vet-api: export GOBIN := $(GOBIN)
vet-api: export ROOT := $(ROOT)
vet-api: ## Run mattermost go vet to verify api4 documentation, currently not passing
$(GO) install github.com/mattermost/mattermost-govet/v2@7d8db289e508999dfcac47b97c9490a0fec12d66
cd ../tools/mattermost-govet && $(GO) install .
make -C ../api build
./scripts/vet-api-check.sh
+1
View File
@@ -4,6 +4,7 @@ This directory aims to provide a set of tools that simplify and enhance various
## Included tools
* **mattermost-govet**: custom Go vet analyzers enforcing Mattermost-specific code conventions (structured logging, error handling, SQL safety, etc.). Used by `make vet` in the server.
* **mmgotool**: is a CLI to help with i18n related checks for the mattermost/server development.
* **sharedchannel-test**: integration test tool that validates shared channel synchronization (posts, reactions, membership) between two real Mattermost server instances.
+9
View File
@@ -0,0 +1,9 @@
version: "2"
linters:
exclusions:
rules:
- path: immut/testdata.go
linters:
- govet
text: "buildtag"
+18
View File
@@ -0,0 +1,18 @@
.PHONY: all test clean golangci-lint check-style
export GOBIN ?= $(PWD)/bin
GO ?= go
all: test
test:
$(GO) test ./...
clean:
rm -rf dist
golangci-lint:
$(GO) install github.com/golangci/golangci-lint/v2/cmd/[email protected]
$(GOBIN)/golangci-lint run ./...
check-style: golangci-lint
+35
View File
@@ -0,0 +1,35 @@
# Mattermost Go Vet
This package contains mattermost-specific go-vet rules that are used to maintain code consistency in `mattermost`.
## Included analyzers
1. **equalLenAsserts** - check for (require/assert).Equal(t, X, len(Y))
1. **inconsistentReceiverName** - check for inconsistent receiver names in the methods of a struct
1. **license** - check the license header
1. **openApiSync** - check for inconsistencies between OpenAPI spec and the source code
1. **structuredLogging** - check invalid usage of logging (must use structured logging)
1. **tFatal** - check invalid usage of t.Fatal assertions (instead of testify methods)
1. **apiAuditLogs** - check that audit records are properly created in the API layer
1. **rawSql** - check invalid usage of raw SQL queries instead of using the squirrel lib
1. **emptyStrCmp** - check for idiomatic empty string comparisons
1. **pointerToSlice** - check for usage of pointer to slice in function definitions
1. **mutexLock** - check for cases where a mutex is left locked before returning
1. **wrapError** - check for original errors being passed as details rather than wrapped
1. **noSelectStar** - check for SQL queries using SELECT * which breaks forwards compatibility
1. **requestCtxNaming** - check that request.CTX parameters are consistently named 'rctx'
## Running Locally
Mattermost Go Vet lives in the `tools/mattermost-govet/` directory of the `mattermost/mattermost` repo. It can also be imported and installed independently for use by plugins or other projects:
```bash
go install github.com/mattermost/mattermost/tools/mattermost-govet@latest
```
To build and run locally from the repo:
```bash
cd tools/mattermost-govet && go install .
go vet -vettool=$(go env GOPATH)/bin/mattermost-govet ./...
```
@@ -0,0 +1,140 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package apiAuditLogs
import (
"go/ast"
"go/token"
"go/types"
"strconv"
"strings"
"github.com/mattermost/mattermost/tools/mattermost-govet/facts"
"github.com/mattermost/mattermost/tools/mattermost-govet/util"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
const mattermostPackagePath = "github.com/mattermost/mattermost-server/v6/"
var Analyzer = &analysis.Analyzer{
Name: "apiAuditLogs",
Doc: "check that audit records are properly created in the API layer.",
Requires: []*analysis.Analyzer{inspect.Analyzer, facts.ApiHandlerFacts},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
if pass.Pkg.Path() != util.API4PkgPath {
return nil, nil
}
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.FuncDecl)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
funDecl := n.(*ast.FuncDecl)
filename := pass.Fset.File(n.Pos()).Name()
if strings.HasSuffix(filename, "_test.go") || strings.HasSuffix(filename, "apitestlib.go") {
return
}
if whiteList[funDecl.Name.Name] {
return
}
if obj, ok := pass.TypesInfo.Defs[funDecl.Name].(*types.Func); ok {
var fact facts.IsApiHandler
if !pass.ImportObjectFact(obj, &fact) {
return
}
}
initializationFound := false
logCallFound := false
successCallFound := false
ast.Inspect(funDecl, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.CallExpr:
fun, ok := n.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
instanceType, ok := pass.TypesInfo.Types[fun.X]
if !ok {
return true
}
if instanceType.Type.String() == "*"+util.ServerModulePath+"/channels/audit.Record" && fun.Sel.Name == "Success" {
successCallFound = true
}
if instanceType.Type.String() == "*"+mattermostPackagePath+"/channels/web.Context" && (fun.Sel.Name == "LogAuditRec" || fun.Sel.Name == "LogAuditRecWithLevel") {
logCallFound = true
}
if instanceType.Type.String() == "*"+mattermostPackagePath+"/channels/web.Context" && fun.Sel.Name == "MakeAuditRecord" {
initializationFound = true
if len(n.Args) < 2 {
pass.Reportf(n.Pos(), "Invalid record initialization, expected at least 2 parameters")
return true
}
arg0, ok := n.Args[0].(*ast.BasicLit)
if !ok {
pass.Reportf(n.Args[0].Pos(), "Invalid record name, expected \"%s\", found \"%v\"", funDecl.Name.Name, n.Args[0])
return true
}
arg1, ok := n.Args[1].(*ast.SelectorExpr)
if !ok {
pass.Reportf(n.Args[1].Pos(), "Invalid initial state for record, expected \"audit.Fail\", found \"%v\"", n.Args[1])
return true
}
if arg0.Kind != token.STRING {
pass.Reportf(n.Args[0].Pos(), "Invalid record name, expected \"%s\", found \"%v\"", funDecl.Name.Name, arg0)
return true
}
arg0Val, err := strconv.Unquote(arg0.Value)
if err != nil {
pass.Reportf(n.Args[0].Pos(), "Invalid record name, expected \"%s\", found \"%v\"", funDecl.Name.Name, arg0.Value)
return true
}
if arg0Val != funDecl.Name.Name {
pass.Reportf(n.Args[0].Pos(), "Invalid record name, expected \"%s\", found \"%s\"", funDecl.Name.Name, arg0Val)
return true
}
arg1X, ok := arg1.X.(*ast.Ident)
if !ok {
pass.Reportf(n.Args[1].Pos(), "Invalid initial state for record, expected \"audit.Fail\", found \"%v\"", arg1.X)
return true
}
if arg1X.Name != "audit" || arg1.Sel.Name != "Fail" {
pass.Reportf(n.Args[1].Pos(), "Invalid initial state for record, expected \"audit.Fail\", found \"%s.%s\"", arg1X.Name, arg1.Sel.Name)
return true
}
}
}
return true
})
if !initializationFound {
pass.Reportf(funDecl.Pos(), "Expected audit log in this function, but not found, please add the audit logs or add the \"%s\" function to the white list", funDecl.Name.Name)
return
}
if !logCallFound {
pass.Reportf(funDecl.Pos(), "Expected LogAuditRec or LogAuditRecWithLevel call, but not found")
}
if !successCallFound {
pass.Reportf(funDecl.Pos(), "Expected Success call, but not found")
}
})
return nil, nil
}
@@ -0,0 +1,163 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package apiAuditLogs
var whiteList = map[string]bool{
"autocompleteChannelsForTeam": true,
"autocompleteChannelsForTeamForSearch": true,
"autocompleteEmojis": true,
"autocompleteUsers": true,
"channelMembersMinusGroupMembers": true,
"checkUserMfa": true,
"connectWebSocket": true,
"createEphemeralPost": true,
"deleteReaction": true,
"doPostAction": true,
"getAllChannels": true,
"getAllTeams": true,
"getAnalytics": true,
"getAuthorizedOAuthApps": true,
"getBot": true,
"getBotIconImage": true,
"getBots": true,
"getBrandImage": true,
"getBulkReactions": true,
"getChannel": true,
"getChannelByName": true,
"getChannelByNameForTeamName": true,
"getChannelMember": true,
"getChannelMembers": true,
"getChannelMembersByIds": true,
"getChannelMembersForUser": true,
"getChannelMembersTimezones": true,
"getChannelModerations": true,
"getChannelsForScheme": true,
"getChannelsForTeamForUser": true,
"getChannelStats": true,
"getChannelUnread": true,
"getClientConfig": true,
"getClientLicense": true,
"getClusterStatus": true,
"getCommand": true,
"getConfig": true,
"getDefaultProfileImage": true,
"getDeletedChannelsForTeam": true,
"getEmoji": true,
"getEmojiByName": true,
"getEmojiImage": true,
"getEmojiList": true,
"getEnvironmentConfig": true,
"getFile": true,
"getFileInfo": true,
"getFileInfosForPost": true,
"getFileLink": true,
"getFilePreview": true,
"getFileThumbnail": true,
"getFlaggedPostsForUser": true,
"getGroup": true,
"getGroupMembers": true,
"getGroups": true,
"getGroupsByChannel": true,
"getGroupsByTeam": true,
"getGroupSyncable": true,
"getGroupSyncables": true,
"getImage": true,
"getIncomingHooks": true,
"getInviteInfo": true,
"getJob": true,
"getJobs": true,
"getJobsByType": true,
"getLatestTermsOfService": true,
"getLdapGroups": true,
"getMarketplacePlugins": true,
"getOAuthApp": true,
"getOAuthAppInfo": true,
"getOAuthApps": true,
"getOpenGraphMetadata": true,
"getOutgoingHooks": true,
"getPinnedPosts": true,
"getPlugins": true,
"getPluginStatuses": true,
"getPolicy": true,
"getPost": true,
"getPostsForChannel": true,
"getPostsForChannelAroundLastUnread": true,
"getPostThread": true,
"getPreferenceByCategoryAndName": true,
"getPreferences": true,
"getPreferencesByCategory": true,
"getProfileImage": true,
"getPublicChannelsByIdsForTeam": true,
"getPublicChannelsForTeam": true,
"getReactions": true,
"getRedirectLocation": true,
"getRole": true,
"getRoleByName": true,
"getRolesByNames": true,
"getSamlCertificateStatus": true,
"getSamlMetadata": true,
"getSamlMetadataFromIdp": true,
"getScheme": true,
"getSchemes": true,
"getServerBusyExpires": true,
"getSessions": true,
"getSupportedTimezones": true,
"getSystemPing": true,
"getTeam": true,
"getTeamByName": true,
"getTeamIcon": true,
"getTeamMember": true,
"getTeamMembers": true,
"getTeamMembersByIds": true,
"getTeamMembersForUser": true,
"getTeamsForScheme": true,
"getTeamsForUser": true,
"getTeamStats": true,
"getTeamsUnreadForUser": true,
"getTeamUnread": true,
"getTotalUsersStats": true,
"getUser": true,
"getUserAccessToken": true,
"getUserAccessTokens": true,
"getUserAccessTokensForUser": true,
"getUserByEmail": true,
"getUserByUsername": true,
"getUsers": true,
"getUsersByGroupChannelIds": true,
"getUsersByIds": true,
"getUsersByNames": true,
"getUserStatus": true,
"getUserStatusesByIds": true,
"getUserTermsOfService": true,
"getWebappPlugins": true,
"listAutocompleteCommands": true,
"listCommands": true,
"openDialog": true,
"patchChannelModerations": true,
"pinPost": true,
"pushNotificationAck": true,
"saveReaction": true,
"searchAllChannels": true,
"searchArchivedChannelsForTeam": true,
"searchChannelsForTeam": true,
"searchEmojis": true,
"searchGroupChannels": true,
"searchPosts": true,
"searchTeams": true,
"searchUserAccessTokens": true,
"searchUsers": true,
"setPostUnread": true,
"submitDialog": true,
"teamExists": true,
"teamMembersMinusGroupMembers": true,
"testElasticsearch": true,
"testEmail": true,
"testLdap": true,
"testS3": true,
"testSiteURL": true,
"unpinPost": true,
"updateUserStatus": true,
"uploadFileStream": true,
"viewChannel": true,
}
@@ -0,0 +1,63 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package appErrorWhere
import (
"fmt"
"go/ast"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "appErrorWhere",
Doc: "check for invalid where value in the appError",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
switch funDecl := node.(type) {
case *ast.FuncDecl:
ast.Inspect(funDecl, func(node ast.Node) bool {
switch x := node.(type) {
case *ast.CallExpr:
fun, ok := x.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
module, ok := fun.X.(*ast.Ident)
if !ok {
return true
}
if fun.Sel.Name != "NewAppError" || module.Name != "model" {
return true
}
if len(x.Args) == 0 {
return true
}
firstArg, ok := x.Args[0].(*ast.BasicLit)
if !ok {
return true
}
if firstArg.Value == fmt.Sprintf("\"%s\"", funDecl.Name.Name) {
return true
}
pass.Reportf(node.Pos(), "The first NewAppError parameter must be the name of the function, seen: %s, expected: %s", firstArg.Value, fmt.Sprintf("\"%s\"", funDecl.Name.Name))
}
return true
})
}
return true
})
}
return nil, nil
}
@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package appErrorWhere
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAll(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
@@ -0,0 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package a
import "a/model"
// Valid: First argument matches function name
func CreateUser() error {
return model.NewAppError("CreateUser", "error.message", nil, "", 500)
}
func UpdateProfile() error {
err := model.NewAppError("UpdateProfile", "error.message", nil, "", 500)
return err
}
func DeleteAccount() error {
// Multiple calls with correct names
if err := model.NewAppError("DeleteAccount", "error1", nil, "", 500); err != nil {
return err
}
return model.NewAppError("DeleteAccount", "error2", nil, "", 400)
}
// Invalid: First argument doesn't match function name
func SaveData() error {
return model.NewAppError("WrongName", "error.message", nil, "", 500) // want "The first NewAppError parameter must be the name of the function"
}
func ProcessRequest() error {
err := model.NewAppError("DifferentFunction", "error.message", nil, "", 500) // want "The first NewAppError parameter must be the name of the function"
return err
}
func HandleSubmit() error {
// One correct, one wrong
if true {
model.NewAppError("HandleSubmit", "error1", nil, "", 500)
}
return model.NewAppError("handleSubmit", "error2", nil, "", 400) // want "The first NewAppError parameter must be the name of the function"
}
// Edge cases
func EmptyFunction() error {
// Empty function name would be caught but we test with wrong name
return model.NewAppError("", "error.message", nil, "", 500) // want "The first NewAppError parameter must be the name of the function"
}
@@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// AppError is a mock of Mattermost's AppError type for testing
type AppError struct {
Where string
Message string
DetailedError string
StatusCode int
}
func (e *AppError) Error() string {
return e.Message
}
// NewAppError creates a new AppError
func NewAppError(where string, message string, params map[string]any, details string, status int) *AppError {
return &AppError{
Where: where,
Message: message,
DetailedError: details,
StatusCode: status,
}
}
@@ -0,0 +1,201 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package concurrentIndex
import (
"go/ast"
"go/token"
"io/fs"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"golang.org/x/tools/go/analysis"
)
const (
diagCreateIndex = "use CREATE INDEX CONCURRENTLY instead of CREATE INDEX to avoid blocking DML"
diagDropIndex = "use DROP INDEX CONCURRENTLY instead of DROP INDEX to avoid blocking DML"
)
var (
createIndexRegex = regexp.MustCompile(`(?i)\bCREATE\s+(UNIQUE\s+)?INDEX\b`)
concurrentlyRegex = regexp.MustCompile(`(?i)\bCREATE\s+(UNIQUE\s+)?INDEX\s+CONCURRENTLY\b`)
dropIndexRegex = regexp.MustCompile(`(?i)\bDROP\s+INDEX\b`)
dropConcurrentlyRegex = regexp.MustCompile(`(?i)\bDROP\s+INDEX\s+CONCURRENTLY\b`)
)
var sqlPath string
var minMigration int
var migrationNumberRegex = regexp.MustCompile(`^\d+`)
var Analyzer = &analysis.Analyzer{
Name: "concurrentIndex",
Doc: "checks that CREATE INDEX and DROP INDEX use CONCURRENTLY to avoid blocking DML",
Run: run,
}
func init() {
Analyzer.Flags.StringVar(&sqlPath, "path", "", "Relative path to a directory of .sql files to scan recursively")
Analyzer.Flags.IntVar(&minMigration, "minMigration", 0, "Skip SQL files with a migration number below this value")
}
func checkStatement(stmt string) string {
if createIndexRegex.MatchString(stmt) && !concurrentlyRegex.MatchString(stmt) {
return diagCreateIndex
}
if dropIndexRegex.MatchString(stmt) && !dropConcurrentlyRegex.MatchString(stmt) {
return diagDropIndex
}
return ""
}
func containsIndexKeyword(s string) bool {
return strings.Contains(strings.ToLower(s), "index")
}
func checkLine(line string) string {
if !containsIndexKeyword(line) {
return ""
}
for _, stmt := range strings.Split(line, ";") {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
if msg := checkStatement(stmt); msg != "" {
return msg
}
}
return ""
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
if strings.HasSuffix(pass.Fset.File(file.Pos()).Name(), "_test.go") {
continue
}
ast.Inspect(file, func(node ast.Node) bool {
lit, ok := node.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
return true
}
val, err := strconv.Unquote(lit.Value)
if err != nil {
val = strings.Trim(lit.Value, "`")
}
if msg := checkLine(val); msg != "" {
pass.Reportf(lit.Pos(), "%s", msg)
}
return true
})
}
if sqlPath != "" {
if err := scanSQLDir(pass, sqlPath); err != nil {
return nil, err
}
}
return nil, nil
}
func getMigrationNumberFromFilename(filename string) int {
m := migrationNumberRegex.FindString(filename)
if m == "" {
return -1
}
n, _ := strconv.Atoi(m)
return n
}
func scanSQLDir(pass *analysis.Pass, root string) error {
return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if d.IsDir() || !strings.HasSuffix(path, ".sql") {
return nil
}
if minMigration > 0 {
if n := getMigrationNumberFromFilename(d.Name()); n >= 0 && n < minMigration {
return nil
}
}
return checkSQLFile(pass, path)
})
}
func checkSQLFile(pass *analysis.Pass, name string) error {
content, err := os.ReadFile(name)
if err != nil {
return err
}
if !containsIndexKeyword(string(content)) {
return nil
}
tf := pass.Fset.AddFile(name, -1, len(content))
tf.SetLinesForContent(content)
lines := strings.Split(string(content), "\n")
var stmtBuf strings.Builder
startLine := 1
for i, line := range lines {
lineNum := i + 1
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if stmtBuf.Len() == 0 {
startLine = lineNum
}
if stmtBuf.Len() > 0 {
stmtBuf.WriteByte(' ')
}
stmtBuf.WriteString(trimmed)
for {
current := stmtBuf.String()
semi := strings.Index(current, ";")
if semi < 0 {
break
}
stmt := strings.TrimSpace(current[:semi])
if stmt != "" {
if msg := checkStatement(stmt); msg != "" {
pass.Reportf(tf.LineStart(startLine), "%s", msg)
}
}
rest := strings.TrimSpace(current[semi+1:])
stmtBuf.Reset()
if rest != "" {
stmtBuf.WriteString(rest)
startLine = lineNum
}
}
}
if stmtBuf.Len() > 0 {
if msg := checkStatement(stmtBuf.String()); msg != "" {
pass.Reportf(tf.LineStart(startLine), "%s", msg)
}
}
return nil
}
@@ -0,0 +1,208 @@
package concurrentIndex
import (
"go/token"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestGoFiles(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
func TestSQLDir(t *testing.T) {
testdata := analysistest.TestData()
migrationsDir := filepath.Join(testdata, "migrations")
fset := token.NewFileSet()
var diags []analysis.Diagnostic
pass := &analysis.Pass{
Analyzer: Analyzer,
Fset: fset,
Report: func(d analysis.Diagnostic) {
diags = append(diags, d)
},
}
err := scanSQLDir(pass, migrationsDir)
require.NoError(t, err)
var files []string
for _, d := range diags {
pos := fset.Position(d.Pos)
files = append(files, filepath.Base(pos.Filename)+":"+d.Message)
}
createCount := 0
dropCount := 0
for _, f := range files {
switch {
case strings.Contains(f, diagCreateIndex):
createCount++
case strings.Contains(f, diagDropIndex):
dropCount++
}
}
assert.Equal(t, 4, createCount, "expected 4 CREATE INDEX diagnostics")
assert.Equal(t, 2, dropCount, "expected 2 DROP INDEX diagnostics")
badCount := 0
nestedCount := 0
multilineCount := 0
multistmtCount := 0
for _, f := range files {
switch {
case strings.HasPrefix(f, "000002_bad"):
badCount++
case strings.HasPrefix(f, "000003_nested"):
nestedCount++
case strings.HasPrefix(f, "000004_multiline"):
multilineCount++
case strings.HasPrefix(f, "000005_multistmt"):
multistmtCount++
}
}
assert.Equal(t, 3, badCount, "expected 3 diagnostics from 000002_bad.up.sql")
assert.Equal(t, 1, nestedCount, "expected 1 diagnostic from sub/000003_nested.up.sql")
assert.Equal(t, 1, multilineCount, "expected 1 diagnostic from 000004_multiline.up.sql")
assert.Equal(t, 1, multistmtCount, "expected 1 diagnostic from 000005_multistmt.up.sql")
for _, f := range files {
assert.NotContains(t, f, "000001_good",
"good migration file should not have diagnostics")
}
}
func TestSQLDirBadPath(t *testing.T) {
fset := token.NewFileSet()
pass := &analysis.Pass{
Analyzer: Analyzer,
Fset: fset,
Report: func(d analysis.Diagnostic) {},
}
err := scanSQLDir(pass, "/nonexistent/path")
assert.NoError(t, err)
}
func TestSQLDirMinMigration(t *testing.T) {
testdata := analysistest.TestData()
migrationsDir := filepath.Join(testdata, "migrations")
old := minMigration
t.Cleanup(func() { minMigration = old })
minMigration = 5
fset := token.NewFileSet()
var diags []analysis.Diagnostic
pass := &analysis.Pass{
Analyzer: Analyzer,
Fset: fset,
Report: func(d analysis.Diagnostic) {
diags = append(diags, d)
},
}
err := scanSQLDir(pass, migrationsDir)
require.NoError(t, err)
for _, d := range diags {
pos := fset.Position(d.Pos)
base := filepath.Base(pos.Filename)
n := getMigrationNumberFromFilename(base)
require.GreaterOrEqual(t, n, 0, "expected numeric prefix in %s", base)
assert.GreaterOrEqual(t, n, 5,
"expected only migrations >= 5, got diagnostic from %s", base)
}
assert.Equal(t, 1, len(diags), "expected 1 diagnostic from migration 5+")
}
func TestCheckLine(t *testing.T) {
tests := []struct {
name string
line string
wantMsg string
}{
{
name: "CREATE INDEX without CONCURRENTLY",
line: "CREATE INDEX IF NOT EXISTS idx_foo_bar ON foo (bar);",
wantMsg: diagCreateIndex,
},
{
name: "CREATE UNIQUE INDEX without CONCURRENTLY",
line: "CREATE UNIQUE INDEX IF NOT EXISTS idx_foo_bar ON foo (bar);",
wantMsg: diagCreateIndex,
},
{
name: "DROP INDEX without CONCURRENTLY",
line: "DROP INDEX IF EXISTS idx_foo_bar;",
wantMsg: diagDropIndex,
},
{
name: "lowercase create index",
line: "create index if not exists idx_foo on foo (bar);",
wantMsg: diagCreateIndex,
},
{
name: "lowercase drop index",
line: "drop index if exists idx_foo;",
wantMsg: diagDropIndex,
},
{
name: "CREATE INDEX CONCURRENTLY is fine",
line: "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_foo_bar ON foo (bar);",
},
{
name: "CREATE UNIQUE INDEX CONCURRENTLY is fine",
line: "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_foo_bar ON foo (bar);",
},
{
name: "DROP INDEX CONCURRENTLY is fine",
line: "DROP INDEX CONCURRENTLY IF EXISTS idx_foo_bar;",
},
{
name: "unrelated SQL",
line: "CREATE TABLE foo (id int);",
},
{
name: "SELECT statement",
line: "SELECT * FROM indexes;",
},
{
name: "plain string",
line: "just a regular string",
},
{
name: "multi-statement with unsafe and safe",
line: "CREATE INDEX idx_a ON t (c); CREATE INDEX CONCURRENTLY idx_b ON t (c);",
wantMsg: diagCreateIndex,
},
{
name: "multi-statement with safe then unsafe drop",
line: "DROP INDEX CONCURRENTLY IF EXISTS idx_a; DROP INDEX IF EXISTS idx_b;",
wantMsg: diagDropIndex,
},
{
name: "multi-statement all safe",
line: "CREATE INDEX CONCURRENTLY idx_a ON t (c); DROP INDEX CONCURRENTLY IF EXISTS idx_b;",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := checkLine(tt.line)
if tt.wantMsg == "" {
assert.Empty(t, msg)
} else {
assert.Equal(t, tt.wantMsg, msg)
}
})
}
}
@@ -0,0 +1,3 @@
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_foo_bar ON foo (bar);
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_foo_baz ON foo (baz);
DROP INDEX CONCURRENTLY IF EXISTS idx_foo_old;
@@ -0,0 +1,3 @@
CREATE INDEX IF NOT EXISTS idx_foo_bar ON foo (bar);
CREATE UNIQUE INDEX IF NOT EXISTS idx_foo_baz ON foo (baz);
DROP INDEX IF EXISTS idx_foo_old;
@@ -0,0 +1,4 @@
CREATE INDEX
IF NOT EXISTS idx_multiline
ON foo (bar);
DROP INDEX CONCURRENTLY IF EXISTS idx_multiline_ok;
@@ -0,0 +1 @@
CREATE INDEX CONCURRENTLY idx_safe ON t (c); CREATE INDEX idx_unsafe ON t (d);
@@ -0,0 +1,2 @@
DROP INDEX idx_nested;
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_nested_ok ON t (c);
@@ -0,0 +1,39 @@
package a
func example() {
// CREATE INDEX without CONCURRENTLY should trigger warnings
_ = "CREATE INDEX IF NOT EXISTS idx_foo_bar ON foo (bar)" // want `use CREATE INDEX CONCURRENTLY instead of CREATE INDEX to avoid blocking DML`
_ = "CREATE INDEX idx_foo_bar ON foo (bar)" // want `use CREATE INDEX CONCURRENTLY instead of CREATE INDEX to avoid blocking DML`
_ = "create index if not exists idx_foo_bar on foo (bar)" // want `use CREATE INDEX CONCURRENTLY instead of CREATE INDEX to avoid blocking DML`
_ = "CREATE UNIQUE INDEX IF NOT EXISTS idx_foo_bar ON foo (bar)" // want `use CREATE INDEX CONCURRENTLY instead of CREATE INDEX to avoid blocking DML`
_ = "create unique index idx_foo_bar on foo (bar)" // want `use CREATE INDEX CONCURRENTLY instead of CREATE INDEX to avoid blocking DML`
// DROP INDEX without CONCURRENTLY should trigger warnings
_ = "DROP INDEX IF EXISTS idx_foo_bar" // want `use DROP INDEX CONCURRENTLY instead of DROP INDEX to avoid blocking DML`
_ = "DROP INDEX idx_foo_bar" // want `use DROP INDEX CONCURRENTLY instead of DROP INDEX to avoid blocking DML`
_ = "drop index if exists idx_foo_bar" // want `use DROP INDEX CONCURRENTLY instead of DROP INDEX to avoid blocking DML`
// CREATE INDEX CONCURRENTLY should not trigger warnings
_ = "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_foo_bar ON foo (bar)"
_ = "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_foo_bar ON foo (bar)"
_ = "create index concurrently if not exists idx_foo_bar on foo (bar)"
_ = "create unique index concurrently idx_foo_bar on foo (bar)"
// DROP INDEX CONCURRENTLY should not trigger warnings
_ = "DROP INDEX CONCURRENTLY IF EXISTS idx_foo_bar"
_ = "drop index concurrently if exists idx_foo_bar"
// Unrelated strings should not trigger warnings
_ = "SELECT * FROM indexes"
_ = "just a regular string"
_ = "CREATE TABLE foo (id int)"
}
func exampleBacktick() {
// Backtick strings should also be checked
_ = `CREATE INDEX IF NOT EXISTS idx_foo_bar ON foo (bar)` // want `use CREATE INDEX CONCURRENTLY instead of CREATE INDEX to avoid blocking DML`
_ = `DROP INDEX IF EXISTS idx_foo_bar` // want `use DROP INDEX CONCURRENTLY instead of DROP INDEX to avoid blocking DML`
_ = `CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_foo_bar ON foo (bar)`
_ = `DROP INDEX CONCURRENTLY IF EXISTS idx_foo_bar`
}
@@ -0,0 +1,129 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package configtelemetry
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/types"
"reflect"
"strings"
"github.com/mattermost/mattermost/tools/mattermost-govet/util"
"golang.org/x/tools/go/analysis"
)
var (
telemetryPkgPath = util.ServerModulePath + "/platform/telemetry"
modelPkgPath = util.ModelPkgPath
)
const (
configStructName = "Config"
telemetryPkgName = "telemetry"
)
// Analyzer reports config fields where telemetry is not sent
var Analyzer = &analysis.Analyzer{
Name: "configtelemetry",
Doc: "reports config fields where telemetry is not sent (unless it is specifically indicated that no telemetry will be sent for that field)",
Run: run,
ResultType: reflect.TypeOf(&analysis.Pass{}),
}
func run(pass *analysis.Pass) (interface{}, error) {
if pass.Pkg.Path() != telemetryPkgPath {
return pass, nil
}
// we need to find model.Config definition to parse its Type Spec
var configObject *types.TypeName
for _, obj := range pass.TypesInfo.Uses {
if tn, ok := obj.(*types.TypeName); ok && obj.Name() == configStructName && obj.Pkg().Path() == modelPkgPath {
configObject = tn
break
}
}
if configObject == nil {
return pass, errors.New("could not find model.Config type")
}
if !configObject.Pos().IsValid() {
return pass, errors.New("model.Config position is not valid")
}
srcFile := pass.Fset.PositionFor(configObject.Pos(), false).Filename
file, err := parser.ParseFile(pass.Fset, srcFile, nil, parser.AllErrors|parser.ParseComments)
if err != nil {
return pass, fmt.Errorf("could parse file %q: %w", srcFile, err)
}
pass.Files = append(pass.Files, file)
configMap, err := typeFieldMap(file, configObject.Name())
if err != nil {
return pass, fmt.Errorf("could generate fields map: %w", err)
}
var allExpressions []string
// We look at every expression in the file so that we can
// check if the expected references exist in this package.
// NOTE: this approach does not check if the reference (config field) is added
// to telemetry queue, it assumes that if a config field is used in the file,
// it probably added to the telemetry. This is a naive approach but should work.
// Also, we can narrow down this for specific function calls, declarations etc.
// so that we can be sure if the config field is added to telemetry but all these
// fine tuning will require extra maintenance cost and yet, it is still possible to
// trick the checker. Hence, it is decided to go with this broader check. This check
// can be considered as a reminder to developers when they forget to add a field to
// telemetry. Fortunately, further verification can be done at code reviews.
for _, file := range pass.Files {
if file.Name.Name != telemetryPkgName {
continue
}
ast.Inspect(file, func(n ast.Node) bool {
expr, ok := n.(ast.Expr)
if !ok {
return true
}
sel, ok := expr.(*ast.SelectorExpr)
if !ok {
return true
}
allExpressions = append(allExpressions, getFieldAccessSequence(sel))
return true
})
}
// now remove the expresions from what's expected
for _, ref := range allExpressions {
delete(configMap, ref)
}
// report remaining fields from the map
for k, v := range configMap {
pass.Reportf(v, "%v is not used in telemetry", k)
}
return pass, nil
}
// getFieldAccessSequence is used recursively to get string representation of
// a selector expression sequence. e.g. config.ServiceSettings.SiteURL
func getFieldAccessSequence(sel *ast.SelectorExpr) string {
switch v := sel.X.(type) {
case *ast.SelectorExpr:
return strings.Join([]string{getFieldAccessSequence(v), sel.Sel.Name}, ".")
default:
return sel.Sel.Name
}
}
@@ -0,0 +1,81 @@
package configtelemetry
import (
"strings"
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
// stubT is a minimal testing.T implementation that doesn't fail
// Used to capture analysistest results without test failures
type stubT struct {
testing.TB
}
func (s *stubT) Errorf(format string, args ...any) {
// Ignore errors from analysistest about unexpected diagnostics
// We validate diagnostics manually below
}
func Test(t *testing.T) {
telemetryPkgPath = "telemetry"
modelPkgPath = "model"
testdata := analysistest.TestData()
// Run analyzer using a stub to prevent analysistest from failing on cross-package diagnostics
// (diagnostics are reported on model package from telemetry package analysis)
stub := &stubT{TB: t}
results := analysistest.Run(stub, testdata, Analyzer, "telemetry")
// Manually verify expected diagnostics
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d", len(results))
}
result := results[0]
if len(result.Diagnostics) == 0 {
t.Fatal("expected diagnostics but got none")
}
// Expected diagnostics: fields in model/config.go that are NOT used in telemetry
expectedDiagnostics := map[string]bool{
"ServiceSettings.WebsocketURL is not used in telemetry": false,
"ServiceSettings.LicenseFileLocation is not used in telemetry": false,
"MessageExportSettings.GlobalRelaySettings.SMTPServerTimeout is not used in telemetry": false,
"CloudSettings.CWSUrl is not used in telemetry": false,
}
// Check that we got all expected diagnostics
for _, diag := range result.Diagnostics {
msg := diag.Message
if _, ok := expectedDiagnostics[msg]; ok {
expectedDiagnostics[msg] = true
// Verify diagnostic is in model/config.go
pos := result.Pass.Fset.Position(diag.Pos)
if !strings.Contains(pos.Filename, "model/config.go") {
t.Errorf("diagnostic %q should be in model/config.go, got %s", msg, pos.Filename)
}
}
}
// Verify all expected diagnostics were found
for msg, found := range expectedDiagnostics {
if !found {
t.Errorf("expected diagnostic not found: %s", msg)
}
}
// Verify we don't have unexpected diagnostics
if len(result.Diagnostics) != len(expectedDiagnostics) {
var unexpectedMsgs []string
for _, diag := range result.Diagnostics {
if _, ok := expectedDiagnostics[diag.Message]; !ok {
unexpectedMsgs = append(unexpectedMsgs, diag.Message)
}
}
if len(unexpectedMsgs) > 0 {
t.Errorf("got %d unexpected diagnostics: %v", len(unexpectedMsgs), unexpectedMsgs)
}
}
}
@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
const (
SERVICE_SETTINGS_DEFAULT_SITE_URL = "http://localhost:8065"
)
type ServiceSettings struct {
SiteURL *string `access:"environment,authentication,write_restrictable"`
WebsocketURL *string `access:"write_restrictable,cloud_restrictable"`
LicenseFileLocation *string `access:"write_restrictable,cloud_restrictable"`
ListenAddress *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none
TLSStrictTransport *bool `access:"write_restrictable,cloud_restrictable"`
TLSStrictTransportMaxAge *int64 `access:"write_restrictable,cloud_restrictable"` // telemetry: none
TLSOverwriteCiphers []string `access:"write_restrictable,cloud_restrictable"`
TrustedProxyIPHeader []string `access:"write_restrictable,cloud_restrictable"` // telemetry: none
}
type GlobalRelayMessageExportSettings struct {
CustomerType *string `access:"compliance"` // must be either A9 or A10, dictates SMTP server url
SmtpUsername string `access:"compliance"`
SMTPServerTimeout *int `access:"compliance"`
}
type MessageExportSettings struct {
ExportFromTimestamp *int64 `access:"compliance"`
BatchSize *int `access:"compliance"`
// formatter-specific settings - these are only expected to be non-nil if ExportFormat is set to the associated format
GlobalRelaySettings *GlobalRelayMessageExportSettings `access:"compliance"`
}
type CloudSettings struct {
CWSUrl *string `access:"environment,write_restrictable"`
}
type Config struct {
ServiceSettings ServiceSettings
MessageExportSettings MessageExportSettings
CloudSettings CloudSettings
}
@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package telemetry
import (
"model"
)
const (
TrackConfigService = "config_service"
TrackConfigMessageExport = "config_message_export"
)
type ServerIface interface {
Config() *model.Config
}
type TelemetryService struct {
srv ServerIface
}
func (ts *TelemetryService) sendTelemetry(event string, properties map[string]interface{}) {
}
func isDefault(setting interface{}, defaultValue interface{}) bool {
return setting == defaultValue
}
func (ts *TelemetryService) trackConfig() {
cfg := ts.srv.Config()
ts.sendTelemetry(TrackConfigService, map[string]interface{}{
"tls_strict_transport": *cfg.ServiceSettings.TLSStrictTransport,
"tls_overwrite_ciphers": len(cfg.ServiceSettings.TLSOverwriteCiphers),
"isdefault_site_url": isDefault(*cfg.ServiceSettings.SiteURL, model.SERVICE_SETTINGS_DEFAULT_SITE_URL),
})
ts.sendTelemetry(TrackConfigMessageExport, map[string]interface{}{
"default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp + 1,
"batch_size": (*cfg.MessageExportSettings.BatchSize == 0),
"global_relay_customer_type": *cfg.MessageExportSettings.GlobalRelaySettings.CustomerType,
"is_default_global_relay_smtp_username": isDefault(cfg.MessageExportSettings.GlobalRelaySettings.SmtpUsername, ""),
})
}
@@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package configtelemetry
import (
"go/ast"
"go/token"
"strings"
)
var ignoreComments = []string{
"telemetry: none",
"Deprecated: do not use",
}
// typeFieldMap will generate map of its all possible selectors and adds them as a key. The value
// is the position of the declaration in the type spec.
func typeFieldMap(file *ast.File, typeName string) (map[string]token.Pos, error) {
var fieldsMap map[string]token.Pos
ast.Inspect(file, func(n ast.Node) bool {
decl, ok := n.(*ast.GenDecl)
if !ok || decl.Tok != token.TYPE || len(decl.Specs) < 1 {
return true
}
spec, ok := decl.Specs[0].(*ast.TypeSpec)
if !ok || spec.Name.Name != typeName {
return true
}
typ, ok := spec.Type.(*ast.StructType)
if !ok {
return true
}
fieldsMap = fields(typ)
return false
})
return fieldsMap, nil
}
func fields(str *ast.StructType) map[string]token.Pos {
structFields := make(map[string]token.Pos)
for _, field := range str.Fields.List {
if field.Comment != nil && ignored(field.Comment) {
continue
}
expr := field.Type
if ptr, ok := field.Type.(*ast.StarExpr); ok {
expr = ptr.X
}
if len(field.Names) != 1 {
panic("unhandled struct field definition")
}
key := field.Names[0].Name
v, ok := expr.(*ast.Ident)
if !ok || v.Obj == nil || v.Obj.Decl == nil {
structFields[key] = field.Pos()
continue
}
typeDecl, ok := v.Obj.Decl.(*ast.TypeSpec)
if !ok {
continue
}
typ, ok := typeDecl.Type.(*ast.StructType)
if !ok {
continue
}
for k, v := range fields(typ) {
structFields[key+"."+k] = v
}
}
return structFields
}
func ignored(comments *ast.CommentGroup) bool {
for _, comment := range comments.List {
for _, ignore := range ignoreComments {
if strings.Contains(comment.Text, ignore) {
return true
}
}
}
return false
}
@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package emptyInterface
import (
"go/ast"
"strings"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "emptyInterface",
Doc: "check for usage of interface{} instead of any",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
checkList := []string{
"Regenerate this file using `make plugin-mocks`.",
"Regenerate this file using `make store-mocks`.",
"Regenerate this file using `make sharedchannel-mocks`.",
}
for _, file := range pass.Files {
comments := ""
for _, commentGroup := range file.Comments {
for _, comment := range commentGroup.List {
comments = comments + comment.Text
}
}
skip := false
for _, check := range checkList {
if strings.Contains(comments, check) {
skip = true
}
}
if skip {
continue
}
ast.Inspect(file, func(node ast.Node) bool {
switch x := node.(type) {
case *ast.InterfaceType:
if len(x.Methods.List) == 0 {
pass.Reportf(x.Pos(), "using 'interface{}', please replace it with 'any'.")
}
return true
}
return true
})
}
return nil, nil
}
@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package emptyInterface
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAll(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
@@ -0,0 +1,77 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package a
// Valid: using any instead of interface{}
func validUsingAny() {
var x any
x = 42
x = "hello"
_ = x
}
func acceptAny(val any) {}
func returnAny() any {
return 42
}
type ValidStruct struct {
Field any
}
// Invalid: using interface{}
func invalidUsingInterface() {
var x interface{} // want "using 'interface\\{\\}', please replace it with 'any'."
x = 42
_ = x
}
func acceptInterface(val interface{}) {} // want "using 'interface\\{\\}', please replace it with 'any'."
func returnInterface() interface{} { // want "using 'interface\\{\\}', please replace it with 'any'."
return 42
}
type InvalidStruct struct {
Field interface{} // want "using 'interface\\{\\}', please replace it with 'any'."
}
// Invalid: multiple empty interfaces
type MultipleEmptyInterfaces struct {
Field1 interface{} // want "using 'interface\\{\\}', please replace it with 'any'."
Field2 interface{} // want "using 'interface\\{\\}', please replace it with 'any'."
Field3 any // Valid
}
func multipleParams(a interface{}, b interface{}, c any) {} // want "using 'interface\\{\\}', please replace it with 'any'." "using 'interface\\{\\}', please replace it with 'any'."
// Valid: non-empty interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
func acceptReader(r Reader) {}
// Invalid: map with interface{} values
var invalidMap = map[string]interface{}{} // want "using 'interface\\{\\}', please replace it with 'any'."
// Valid: map with any values
var validMap = map[string]any{}
// Invalid: slice of interface{}
var invalidSlice []interface{} // want "using 'interface\\{\\}', please replace it with 'any'."
// Valid: slice of any
var validSlice []any
// Invalid: channel of interface{}
var invalidChan chan interface{} // want "using 'interface\\{\\}', please replace it with 'any'."
// Valid: channel of any
var validChan chan any
@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package emptyStrCmp
import (
"go/ast"
"go/token"
"strings"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "emptyStrCmp",
Doc: "check for len(s) == 0 and len(s) != 0 where s is string",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
const bindataHeader = "by go-bindata DO NOT EDIT. (@generated)"
for _, file := range pass.Files {
if len(file.Comments) > 0 && strings.HasSuffix(file.Comments[0].List[0].Text, bindataHeader) {
continue
}
ast.Inspect(file, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.BinaryExpr:
callExpr, ok := n.X.(*ast.CallExpr)
if !ok {
return true
}
idt, ok := callExpr.Fun.(*ast.Ident)
if !ok {
return true
}
if idt.Name == "len" && (n.Op == token.EQL || n.Op == token.NEQ || n.Op == token.GTR || n.Op == token.LEQ) {
if len(callExpr.Args) < 1 {
return true
}
arg0 := callExpr.Args[0]
typ, ok := pass.TypesInfo.Types[arg0]
if ok && typ.Type.String() == "string" {
bLit, ok := n.Y.(*ast.BasicLit)
if !ok {
return true
}
if bLit.Value == "0" {
switch n.Op {
case token.EQL:
pass.Reportf(callExpr.Pos(), "calling len(s) == 0 where s is string, please use s == \"\" instead")
case token.NEQ:
pass.Reportf(callExpr.Pos(), "calling len(s) != 0 where s is string, please use s != \"\" instead")
case token.GTR:
pass.Reportf(callExpr.Pos(), "calling len(s) > 0 where s is string, please use s != \"\" instead")
case token.LEQ:
pass.Reportf(callExpr.Pos(), "calling len(s) <= 0 where s is string, please use s == \"\" instead")
}
return false
}
}
}
}
return true
})
}
return nil, nil
}
@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package emptyStrCmp
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAll(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package a
// Valid: idiomatic empty string comparisons
func validEmptyStringChecks() {
var s string
// Valid: direct comparison with empty string
if s == "" {
// This is idiomatic
}
if s != "" {
// This is idiomatic
}
str := "hello"
if str == "" {
// This is idiomatic
}
if str != "" {
// This is idiomatic
}
}
// Invalid: using len for empty string checks
func invalidEmptyStringChecks() {
var s string
// Invalid: len(s) == 0
if len(s) == 0 { // want "calling len\\(s\\) == 0 where s is string, please use s == \\\"\\\" instead"
// Should use s == "" instead
}
// Invalid: len(s) != 0
if len(s) != 0 { // want "calling len\\(s\\) != 0 where s is string, please use s != \\\"\\\" instead"
// Should use s != "" instead
}
str := "hello"
// Invalid: len(str) == 0
if len(str) == 0 { // want "calling len\\(s\\) == 0 where s is string, please use s == \\\"\\\" instead"
// Should use str == "" instead
}
// Invalid: len(str) != 0
if len(str) != 0 { // want "calling len\\(s\\) != 0 where s is string, please use s != \\\"\\\" instead"
// Should use str != "" instead
}
// Invalid: len(s) > 0
if len(s) > 0 { // want "calling len\\(s\\) > 0 where s is string, please use s != \\\"\\\" instead"
// Should use s != "" instead
}
// Invalid: len(s) <= 0
if len(s) <= 0 { // want "calling len\\(s\\) <= 0 where s is string, please use s == \\\"\\\" instead"
// Should use s == "" instead
}
}
// Valid: len for non-zero comparisons
func validLenComparisons() {
s := "hello"
// Valid: comparing length to non-zero values
if len(s) == 5 {
// This is fine
}
if len(s) > 5 {
// This is fine
}
if len(s) < 10 {
// This is fine
}
}
// Valid: len for slices
func validSliceLenComparisons() {
slice := []int{1, 2, 3}
// Valid: len on slices (not strings)
if len(slice) == 0 {
// This is acceptable for slices
}
if len(slice) != 0 {
// This is acceptable for slices
}
}
@@ -0,0 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package equalLenAsserts
import (
"go/ast"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "equalLenAsserts",
Doc: "check for (require/assert).Equal(t, X, len(Y))",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
switch x := node.(type) {
case *ast.CallExpr:
callExpr, _ := node.(*ast.CallExpr)
fun, ok := x.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
module, ok := fun.X.(*ast.Ident)
if !ok {
return true
}
if fun.Sel.Name == "Equal" && (module.Name == "require" || module.Name == "assert") {
hasLen := false
hasZero := false
for _, arg := range callExpr.Args {
literal, ok := arg.(*ast.BasicLit)
if ok {
if literal.Value == "0" {
hasZero = true
}
}
call, ok := arg.(*ast.CallExpr)
if ok {
callFun, ok := call.Fun.(*ast.Ident)
if ok {
if callFun.Name == "len" {
hasLen = true
}
}
}
}
if hasLen && hasZero {
pass.Reportf(callExpr.Pos(), "calling len inside require/assert.Equal and comparing to 0, please use require/assert.Empty instead")
return false
}
if hasLen {
pass.Reportf(callExpr.Pos(), "calling len inside require/assert.Equal, please use require/assert.Len instead")
return false
}
}
if fun.Sel.Name == "Len" && (module.Name == "require" || module.Name == "assert") {
if len(callExpr.Args) < 3 {
return true
}
literal, ok := callExpr.Args[2].(*ast.BasicLit)
if ok {
if literal.Value == "0" {
pass.Reportf(callExpr.Pos(), "calling require/assert.Len comparing to 0, please use require/assert.Empty instead")
}
}
}
return true
}
return true
})
}
return nil, nil
}
@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package equalLenAsserts
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAll(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
@@ -0,0 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package a
import "testing"
// Mock testify modules
type require struct{}
type assert struct{}
func (r *require) Equal(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) {}
func (r *require) Len(t *testing.T, object interface{}, length int, msgAndArgs ...interface{}) {}
func (r *require) Empty(t *testing.T, object interface{}, msgAndArgs ...interface{}) {}
func (a *assert) Equal(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) {}
func (a *assert) Len(t *testing.T, object interface{}, length int, msgAndArgs ...interface{}) {}
func (a *assert) Empty(t *testing.T, object interface{}, msgAndArgs ...interface{}) {}
var Require = &require{}
var Assert = &assert{}
// Valid: Using Len correctly
func TestValidUsingLen(t *testing.T) {
require := Require
assert := Assert
slice := []int{1, 2, 3}
require.Len(t, slice, 3)
assert.Len(t, slice, 5)
str := "hello"
require.Len(t, str, 5)
assert.Len(t, str, 5)
}
// Valid: Using Empty correctly
func TestValidUsingEmpty(t *testing.T) {
require := Require
assert := Assert
emptySlice := []int{}
require.Empty(t, emptySlice)
assert.Empty(t, emptySlice)
emptyStr := ""
require.Empty(t, emptyStr)
assert.Empty(t, emptyStr)
}
// Invalid: Using Equal with len (require)
func TestInvalidRequireEqualWithLen(t *testing.T) {
require := Require
slice := []int{1, 2, 3}
// Invalid: Equal with len
require.Equal(t, 3, len(slice)) // want "calling len inside require/assert.Equal, please use require/assert.Len instead"
require.Equal(t, len(slice), 3) // want "calling len inside require/assert.Equal, please use require/assert.Len instead"
str := "hello"
require.Equal(t, 5, len(str)) // want "calling len inside require/assert.Equal, please use require/assert.Len instead"
}
// Invalid: Using Equal with len(0) (require)
func TestInvalidRequireEqualWithLenZero(t *testing.T) {
require := Require
emptySlice := []int{}
// Invalid: Equal with len and 0
require.Equal(t, 0, len(emptySlice)) // want "calling len inside require/assert.Equal and comparing to 0, please use require/assert.Empty instead"
require.Equal(t, len(emptySlice), 0) // want "calling len inside require/assert.Equal and comparing to 0, please use require/assert.Empty instead"
}
// Invalid: Using Len with 0 (require)
func TestInvalidRequireLenWithZero(t *testing.T) {
require := Require
emptySlice := []int{}
// Invalid: Len with 0
require.Len(t, emptySlice, 0) // want "calling require/assert.Len comparing to 0, please use require/assert.Empty instead"
}
// Invalid: Using Equal with len (assert)
func TestInvalidAssertEqualWithLen(t *testing.T) {
assert := Assert
slice := []int{1, 2, 3}
// Invalid: Equal with len
assert.Equal(t, 3, len(slice)) // want "calling len inside require/assert.Equal, please use require/assert.Len instead"
assert.Equal(t, len(slice), 3) // want "calling len inside require/assert.Equal, please use require/assert.Len instead"
str := "hello"
assert.Equal(t, 5, len(str)) // want "calling len inside require/assert.Equal, please use require/assert.Len instead"
}
// Invalid: Using Equal with len(0) (assert)
func TestInvalidAssertEqualWithLenZero(t *testing.T) {
assert := Assert
emptySlice := []int{}
// Invalid: Equal with len and 0
assert.Equal(t, 0, len(emptySlice)) // want "calling len inside require/assert.Equal and comparing to 0, please use require/assert.Empty instead"
assert.Equal(t, len(emptySlice), 0) // want "calling len inside require/assert.Equal and comparing to 0, please use require/assert.Empty instead"
}
// Invalid: Using Len with 0 (assert)
func TestInvalidAssertLenWithZero(t *testing.T) {
assert := Assert
emptySlice := []int{}
// Invalid: Len with 0
assert.Len(t, emptySlice, 0) // want "calling require/assert.Len comparing to 0, please use require/assert.Empty instead"
}
// Valid: Equal without len
func TestValidEqualWithoutLen(t *testing.T) {
require := Require
assert := Assert
x := 5
y := 5
require.Equal(t, x, y)
assert.Equal(t, x, y)
require.Equal(t, 0, x-5)
assert.Equal(t, 0, x-5)
}
@@ -0,0 +1,71 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package errorAssertions
import (
"go/ast"
"github.com/mattermost/mattermost/tools/mattermost-govet/util"
"golang.org/x/tools/go/analysis"
)
var (
appErrorType = util.AppErrType
)
var Analyzer = &analysis.Analyzer{
Name: "errorAssertions",
Doc: "check for (require/assert).Nil/NotNil(t, error)",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
checks := []struct {
AssertionName string
ForbiddenTypeName string
ExpectedAssertion string
}{
{"Nil", "error", "NoError"},
{"NotNil", "error", "Error"},
{"Nilf", "error", "NoErrorf"},
{"NotNilf", "error", "Errorf"},
{"Error", appErrorType, "NotNil"},
{"NoError", appErrorType, "Nil"},
{"Errorf", appErrorType, "NotNilf"},
{"NoErrorf", appErrorType, "Nilf"},
}
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
switch x := node.(type) {
case *ast.CallExpr:
callExpr, _ := node.(*ast.CallExpr)
fun, ok := x.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
idnt, ok := fun.X.(*ast.Ident)
if !ok {
return true
}
if (idnt.Name == "require" || idnt.Name == "assert") && len(callExpr.Args) > 1 {
for _, check := range checks {
if fun.Sel.Name == check.AssertionName {
if typeAndValue, ok := pass.TypesInfo.Types[callExpr.Args[1]]; ok && typeAndValue.Type.String() == check.ForbiddenTypeName {
pass.Reportf(callExpr.Pos(), "calling %s.%s on %s, please use %s.%s instead", idnt.Name, check.AssertionName, check.ForbiddenTypeName, idnt.Name, check.ExpectedAssertion)
return false
}
}
}
}
return true
}
return true
})
}
return nil, nil
}
@@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package errorAssertions
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func Test(t *testing.T) {
appErrorType = "*model.AppError"
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "model", "assert", "errorAssertions")
}
@@ -0,0 +1,14 @@
package require
import (
"testing"
)
func Error(t *testing.T, err error, msgAndArgs ...interface{}) {}
func Errorf(t *testing.T, err error, msg string, args ...interface{}) {}
func NoError(t *testing.T, err error, msgAndArgs ...interface{}) {}
func NoErrorf(t *testing.T, err error, msg string, args ...interface{}) {}
func Nil(t *testing.T, object interface{}, msgAndArgs ...interface{}) {}
func NotNil(t *testing.T, object interface{}, msgAndArgs ...interface{}) {}
func Nilf(t *testing.T, object interface{}, msg string, args ...interface{}) {}
func NotNilf(t *testing.T, object interface{}, msg string, args ...interface{}) {}
@@ -0,0 +1,51 @@
package errorAssertions
import (
"testing"
"assert"
"model"
"require"
)
func TestError(t *testing.T) {
var err error
assert.Error(t, err)
assert.Errorf(t, err, "%s", "foo")
assert.NoError(t, err)
assert.NoErrorf(t, err, "%s", "foo")
assert.Nil(t, err) // want `calling assert.Nil on error, please use assert.NoError instead`
assert.Nilf(t, err, "%s", "foo") // want `calling assert.Nilf on error, please use assert.NoErrorf instead`
assert.NotNil(t, err) // want `calling assert.NotNil on error, please use assert.Error instead`
assert.NotNilf(t, err, "%s", "foo") // want `calling assert.NotNilf on error, please use assert.Errorf instead`
require.Error(t, err)
require.Errorf(t, err, "%s", "foo")
require.NoError(t, err)
require.NoErrorf(t, err, "%s", "foo")
require.Nil(t, err) // want `calling require.Nil on error, please use require.NoError instead`
require.Nilf(t, err, "%s", "foo") // want `calling require.Nilf on error, please use require.NoErrorf instead`
require.NotNil(t, err) // want `calling require.NotNil on error, please use require.Error instead`
require.NotNilf(t, err, "%s", "foo") // want `calling require.NotNilf on error, please use require.Errorf instead`
}
func TestModelAppError(t *testing.T) {
var appErr *model.AppError
assert.Error(t, appErr) // want `calling assert.Error on \*model.AppError, please use assert.NotNil instead`
assert.Errorf(t, appErr, "%s", "foo") // want `calling assert.Errorf on \*model.AppError, please use assert.NotNilf instead`
assert.NoError(t, appErr) // want `calling assert.NoError on \*model.AppError, please use assert.Nil instead`
assert.NoErrorf(t, appErr, "%s", "foo") // want `calling assert.NoErrorf on \*model.AppError, please use assert.Nilf instead`
assert.Nil(t, appErr)
assert.Nilf(t, appErr, "%s", "foo")
assert.NotNil(t, appErr)
assert.NotNilf(t, appErr, "%s", "foo")
require.Error(t, appErr) // want `calling require.Error on \*model.AppError, please use require.NotNil instead`
require.Errorf(t, appErr, "%s", "foo") // want `calling require.Errorf on \*model.AppError, please use require.NotNilf instead`
require.NoError(t, appErr) // want `calling require.NoError on \*model.AppError, please use require.Nil instead`
require.NoErrorf(t, appErr, "%s", "foo") // want `calling require.NoErrorf on \*model.AppError, please use require.Nilf instead`
require.Nil(t, appErr)
require.Nilf(t, appErr, "%s", "foo")
require.NotNil(t, appErr)
require.NotNilf(t, appErr, "%s", "foo")
}
@@ -0,0 +1,7 @@
package model
type AppError struct{}
func (a *AppError) Error() string {
return "an error"
}
@@ -0,0 +1,14 @@
package assert
import (
"testing"
)
func Error(t *testing.T, err error, msgAndArgs ...interface{}) {}
func Errorf(t *testing.T, err error, msg string, args ...interface{}) {}
func NoError(t *testing.T, err error, msgAndArgs ...interface{}) {}
func NoErrorf(t *testing.T, err error, msg string, args ...interface{}) {}
func Nil(t *testing.T, object interface{}, msgAndArgs ...interface{}) {}
func NotNil(t *testing.T, object interface{}, msgAndArgs ...interface{}) {}
func Nilf(t *testing.T, object interface{}, msg string, args ...interface{}) {}
func NotNilf(t *testing.T, object interface{}, msg string, args ...interface{}) {}
@@ -0,0 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package errorVars
import (
"go/ast"
"strings"
"github.com/mattermost/mattermost/tools/mattermost-govet/util"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "errorVars",
Doc: "check for non valid type assignments to err and appErr prefixed variables",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
switch x := node.(type) {
case *ast.AssignStmt:
for idx, lhsExpr := range x.Lhs {
if typeAndValue, ok := pass.TypesInfo.Types[lhsExpr]; ok && typeAndValue.Type.String() == "error" {
if len(x.Rhs) == 1 {
// This is needed to extract the type name string a multi-value return type
if typeAndValue, ok := pass.TypesInfo.Types[x.Rhs[0]]; ok {
returnTypes := strings.Split(strings.Trim(typeAndValue.Type.String(), "()"), ", ")
if len(returnTypes) > idx && returnTypes[idx] == util.AppErrType {
pass.Reportf(x.Pos(), "assigning a *model.AppError to a `error` type variable, please create a new variable to store this value.")
}
}
} else if len(x.Rhs) == len(x.Lhs) {
if typeAndValue, ok := pass.TypesInfo.Types[x.Rhs[idx]]; ok && typeAndValue.Type.String() == util.AppErrType {
pass.Reportf(x.Pos(), "assigning a *model.AppError to a `error` type variable, please create a new variable to store this value.")
}
}
}
}
return true
}
return true
})
}
return nil, nil
}
@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package errorVars
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAll(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package a
import (
"github.com/mattermost/mattermost/server/public/model"
)
// Valid: Assigning AppError to AppError variable
func validAppErrorToAppError() {
var appErr *model.AppError
appErr = model.NewAppError("test", "error", nil, "", 500)
_ = appErr
// Valid: Using := with AppError type
appErr2 := model.NewAppError("test", "error", nil, "", 500)
_ = appErr2
// Valid: Multi-return where AppError goes to AppError variable
_, appErr3 := functionReturningAppError()
_ = appErr3
}
// Valid: Assigning regular error to error variable
func validErrorToError() {
var err error
err = functionReturningError()
_ = err
// Valid: Using := with error type
err2 := functionReturningError()
_ = err2
// Valid: Multi-return where error goes to error variable
_, err3 := functionReturningMultiError()
_ = err3
}
// Valid: Assigning nil to error variable
func validNilAssignments() {
var err error
err = nil
_ = err
var appErr *model.AppError
appErr = nil
_ = appErr
}
// Invalid: Assigning AppError to error variable (multi-return with single RHS)
func invalidMultiReturnAppErrorToError() {
// This is the most common pattern: function returns (something, *model.AppError)
// but we assign it to (something, error)
var err error
_, err = functionReturningAppError() // want "assigning a .* to a"
_ = err
// Similar pattern with different return position
var result string
var err2 error
result, err2 = functionReturningAppError() // want "assigning a .* to a"
_, _ = result, err2
}
// Invalid: Mixed return types
func invalidMixedReturnTypes() {
// Mixed: first is error, second is AppError assigned to error
var normalErr error
var appErr2 error
normalErr, appErr2 = functionReturningErrorAndAppError() // want "assigning a .* to a"
_, _ = normalErr, appErr2
// Another function returning just string and AppError
var data string
var err error
data, err = functionReturningAppError() // want "assigning a .* to a"
_, _ = data, err
}
// Helper functions to simulate different return patterns
func functionReturningError() error {
return nil
}
func functionReturningMultiError() (string, error) {
return "", nil
}
func functionReturningAppError() (string, *model.AppError) {
return "", nil
}
func functionReturningTwoAppErrors() (*model.AppError, *model.AppError) {
return nil, nil
}
func functionReturningErrorAndAppError() (error, *model.AppError) {
return nil, nil
}
@@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// AppError is a mock of Mattermost's AppError type for testing
type AppError struct {
Id string
Message string
DetailedError string
StatusCode int
}
func (e *AppError) Error() string {
return e.Message
}
// NewAppError creates a new AppError
func NewAppError(where string, message string, params map[string]any, details string, status int) *AppError {
return &AppError{
Id: where,
Message: message,
DetailedError: details,
StatusCode: status,
}
}
@@ -0,0 +1,71 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package errorVarsName
import (
"go/ast"
"strings"
"github.com/mattermost/mattermost/tools/mattermost-govet/util"
"golang.org/x/tools/go/analysis"
)
const (
appErrorString = "*" + util.ModelPkgPath + ".AppError"
)
var Analyzer = &analysis.Analyzer{
Name: "errorVarsName",
Doc: "check for err and appErr var names corresponding to the expected types",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
switch x := node.(type) {
case *ast.AssignStmt:
for idx, lhsExpr := range x.Lhs {
idnt, ok := lhsExpr.(*ast.Ident)
if !ok {
return true
}
if strings.HasPrefix(idnt.Name, "err") {
if len(x.Rhs) == 1 {
if typeAndValue, ok := pass.TypesInfo.Types[x.Rhs[0]]; ok {
// This is needed to extract the type name string a multi-value return type
returnTypes := strings.Split(strings.Trim(typeAndValue.Type.String(), "()"), ", ")
if len(returnTypes) > idx && returnTypes[idx] == appErrorString {
pass.Reportf(x.Pos(), "assigning a *model.AppError variable to a `err` prefixed variable, please use `appErr` prefixed variable name instead.")
}
}
} else if len(x.Rhs) == len(x.Lhs) {
if typeAndValue, ok := pass.TypesInfo.Types[x.Rhs[idx]]; ok && typeAndValue.Type.String() == appErrorString {
pass.Reportf(x.Pos(), "assigning a *model.AppError variable to a `err` prefixed variable, please use `appErr` prefixed variable name instead.")
}
}
} else if strings.HasPrefix(idnt.Name, "appErr") {
if len(x.Rhs) == 1 {
if typeAndValue, ok := pass.TypesInfo.Types[x.Rhs[0]]; ok {
// This is needed to extract the type name string a multi-value return type
returnTypes := strings.Split(strings.Trim(typeAndValue.Type.String(), "()"), ", ")
if len(returnTypes) > idx && returnTypes[idx] == "error" {
pass.Reportf(x.Pos(), "assigning a error variable to an `appErr` prefixed variable, please use `err` prefixed variable name instead.")
}
}
} else if len(x.Rhs) == len(x.Lhs) {
if typeAndValue, ok := pass.TypesInfo.Types[x.Rhs[idx]]; ok && typeAndValue.Type.String() == "error" {
pass.Reportf(x.Pos(), "assigning a error variable to an `appErr` prefixed variable, please use `err` prefixed variable name instead.")
}
}
}
}
return true
}
return true
})
}
return nil, nil
}
@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package errorVarsName
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAll(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
@@ -0,0 +1,83 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package a
import (
"github.com/mattermost/mattermost/server/public/model"
)
// Valid: err variable with error type
func validErrWithError() {
var err error
err = functionReturningError()
_ = err
var err2 error
_, err2 = functionReturningMultiError()
_ = err2
// Multiple err variables
var errOne, errTwo error
errOne, errTwo = functionReturningTwoErrors()
_, _ = errOne, errTwo
}
// Valid: appErr variable with AppError type
func validAppErrWithAppError() {
var appErr *model.AppError
appErr = model.NewAppError("test", "error", nil, "", 500)
_ = appErr
var appErr2 *model.AppError
_, appErr2 = functionReturningAppError()
_ = appErr2
// Multiple appErr variables
var appErrOne, appErrTwo *model.AppError
appErrOne, appErrTwo = functionReturningTwoAppErrors()
_, _ = appErrOne, appErrTwo
}
// Invalid: err variable with AppError type (should use appErr)
func invalidErrWithAppError() {
var err *model.AppError
err = model.NewAppError("test", "error", nil, "", 500) // want "assigning a .* to a `err` prefixed variable"
_ = err
var errSomething *model.AppError
_, errSomething = functionReturningAppError() // want "assigning a .* to a `err` prefixed variable"
_ = errSomething
}
// Invalid: appErr variable with error type (should use err)
func invalidAppErrWithError() {
var appErr error
appErr = functionReturningError() // want "assigning a error variable to an `appErr` prefixed variable"
_ = appErr
var appErrSomething error
_, appErrSomething = functionReturningMultiError() // want "assigning a error variable to an `appErr` prefixed variable"
_ = appErrSomething
}
// Helper functions to simulate different return patterns
func functionReturningError() error {
return nil
}
func functionReturningMultiError() (string, error) {
return "", nil
}
func functionReturningTwoErrors() (error, error) {
return nil, nil
}
func functionReturningAppError() (string, *model.AppError) {
return "", nil
}
func functionReturningTwoAppErrors() (*model.AppError, *model.AppError) {
return nil, nil
}
@@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// AppError is a mock of Mattermost's AppError type for testing
type AppError struct {
Id string
Message string
DetailedError string
StatusCode int
}
func (e *AppError) Error() string {
return e.Message
}
// NewAppError creates a new AppError
func NewAppError(where string, message string, params map[string]any, details string, status int) *AppError {
return &AppError{
Id: where,
Message: message,
DetailedError: details,
StatusCode: status,
}
}
@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package facts
import (
"go/ast"
"go/types"
"github.com/mattermost/mattermost/tools/mattermost-govet/util"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
type IsApiHandler struct{}
func (*IsApiHandler) AFact() {}
func (*IsApiHandler) String() string { return "isApiHandler" }
var ApiHandlerFacts = &analysis.Analyzer{
Name: "apiHandlers",
Doc: "check for API handlers and label them with facts",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: apiHandlers,
FactTypes: []analysis.Fact{new(IsApiHandler)},
}
func apiHandlers(pass *analysis.Pass) (interface{}, error) {
if pass.Pkg.Path() != util.API4PkgPath {
return nil, nil
}
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.FuncDecl)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
x := n.(*ast.FuncDecl)
if isApiHandler(x) {
if obj, ok := pass.TypesInfo.Defs[x.Name].(*types.Func); ok {
pass.ExportObjectFact(obj, new(IsApiHandler))
}
}
})
return nil, nil
}
func isApiHandler(funDecl *ast.FuncDecl) bool {
funcType := funDecl.Type
if len(funcType.Params.List) < 3 {
return false
}
arg0Type, ok := funcType.Params.List[0].Type.(*ast.StarExpr)
if !ok {
return false
}
arg0X, ok := arg0Type.X.(*ast.Ident)
if !ok {
return false
}
if arg0X.Name != "Context" {
return false
}
arg1Type, ok := funcType.Params.List[1].Type.(*ast.SelectorExpr)
if !ok {
return false
}
arg1X, ok := arg1Type.X.(*ast.Ident)
if !ok {
return false
}
if arg1X.Name != "http" || arg1Type.Sel.Name != "ResponseWriter" {
return false
}
arg2Type, ok := funcType.Params.List[2].Type.(*ast.StarExpr)
if !ok {
return false
}
arg2X, ok := arg2Type.X.(*ast.SelectorExpr)
if !ok {
return false
}
arg2XX, ok := arg2X.X.(*ast.Ident)
if !ok {
return false
}
if arg2XX.Name != "http" || arg2X.Sel.Name != "Request" {
return false
}
return true
}
+28
View File
@@ -0,0 +1,28 @@
module github.com/mattermost/mattermost/tools/mattermost-govet
go 1.25.8
require (
github.com/getkin/kin-openapi v0.133.0
github.com/pkg/errors v0.9.1
github.com/sajari/fuzzy v1.0.0
github.com/stretchr/testify v1.10.0
golang.org/x/tools v0.40.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+48
View File
@@ -0,0 +1,48 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+446
View File
@@ -0,0 +1,446 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Package immut implements immutable types.
package immut
/*
A type T may be marked as immutable. Being immutable means that
- none of its fields may be written to
- none of its elements (for slices or maps) may be written to, and elements may not be deleted (for maps)
- references stored in the struct may not be modified and have the same protections as explicitly marked immutable types
This makes immutability inherently recursive, and we can't rely
strictly on type analysis. Instead, we must track values, across
function calls.
It also means that we need to track non-reference types. A
dereferenced struct might contain fields of reference types (e.g.
slices), and these stay immutable even if the struct itself is no
longer mutable.
Immutable types can only be constructed in whitelisted functions,
typically one for creation and one for cloning.
Because we have to track values in an analysis model that analyses one
package at a time, we need to compute summaries for functions and
whether they modify any of their arguments. For example, a function
func modify(x []int) { x[0] = 1 }
by itself is perfectly fine, unless it gets called with a slice
derived from an immutable type. This analysis has to occur recursively.
This analysis can inherently not be free of false negatives, unless we
used some form of expensive alias analysis, which would have false
positives instead. As such, the check happens on a best effort basis.
Where more accuracy is desired, the user can opt for using more types
with annotations, but even the type-based analysis can be defeated by
interfaces and other dynamic behavior.
We should also track immutability across type conversions.
*/
import (
"fmt"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/buildssa"
"golang.org/x/tools/go/ssa"
)
const debug = false
func debugf(f string, v ...interface{}) {
if debug {
fmt.Printf(f+"\n", v...)
}
}
var Analyzer = &analysis.Analyzer{
Name: "mutates",
Doc: "Annotates functions that mutate their parameters",
Run: run,
Requires: []*analysis.Analyzer{buildssa.Analyzer},
FactTypes: []analysis.Fact{(*mutatesParametersFact)(nil), (*isImmutable)(nil), (*isConstructor)(nil)},
}
type isImmutable struct{}
type isConstructor struct{}
type mutatesParametersFact struct {
Params []mutationStack
}
func (*isImmutable) AFact() {}
func (*isConstructor) AFact() {}
func (*mutatesParametersFact) AFact() {}
// Functions that are known mutators, but that static analysis can't
// detect, for example because of syscalls.
var knownMutators = map[string]struct{}{
"(*os.File).Read": {},
}
func run(pass *analysis.Pass) (interface{}, error) {
// mark immutable types and constructors
parseDirectives(pass)
// mark all functions that mutate their arguments
markMutators(pass)
// now find immutability violations
checkTypes(pass)
return nil, nil
}
func parseDirectives(pass *analysis.Pass) {
const (
prefixType = "//immut:type "
prefixCtr = "//immut:constructor "
)
for _, f := range pass.Files {
for _, cg := range f.Comments {
for _, c := range cg.List {
if strings.HasPrefix(c.Text, prefixType) {
name := c.Text[len(prefixType):]
obj := pass.Pkg.Scope().Lookup(name)
if obj == nil {
pass.Reportf(c.Pos(), "couldn't find type named %q", name)
} else if tname, ok := obj.(*types.TypeName); ok {
debugf("mark %s immutable", obj)
pass.ExportObjectFact(tname, &isImmutable{})
} else {
// TODO(dh): user friendly output instead of %T
pass.Reportf(c.Pos(), "object %q is a %T, not a type name", name, obj)
}
} else if strings.HasPrefix(c.Text, prefixCtr) {
// TODO(dh): verify that the constructor actually returns a type that is immutable
parts := strings.Split(c.Text[len(prefixCtr):], ".")
switch len(parts) {
case 1:
fnName := parts[0]
obj := pass.Pkg.Scope().Lookup(fnName)
if obj == nil {
// TODO(dh): flag comment
continue
}
fn, ok := obj.(*types.Func)
if !ok {
// TODO(dh): flag comment
continue
}
debugf("mark %s as constructor", fn)
pass.ExportObjectFact(fn, &isConstructor{})
case 2:
typ := parts[0]
methName := parts[1]
obj := pass.Pkg.Scope().Lookup(typ)
if obj == nil {
// TODO(dh): flag comment
continue
}
tname, ok := obj.(*types.TypeName)
if !ok {
// TODO(dh): flag comment
continue
}
named := tname.Type().(*types.Named)
for n, i := named.NumMethods(), 0; i < n; i++ {
if meth := named.Method(i); meth.Name() == methName {
debugf("mark %s as constructor", meth)
pass.ExportObjectFact(meth, &isConstructor{})
break
}
}
default:
pass.Reportf(c.Pos(), "malformed //immut:constructor directive: expected 'FuncName' or 'Type.MethodName'")
continue
}
}
}
}
}
}
func markMutators(pass *analysis.Pass) {
seen := map[*ssa.Function]struct{}{}
for _, fn := range pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA).SrcFuncs {
markMutator(pass, fn, seen)
}
}
func markMutator(pass *analysis.Pass, fn *ssa.Function, seenFns map[*ssa.Function]struct{}) (out []mutationStack) {
if _, ok := knownMutators[fn.String()]; ok {
return []mutationStack{{{Kind: "well-known"}}}
}
if fn.Object() == nil {
// TODO(dh): support closures
return nil
}
if fact := new(mutatesParametersFact); pass.ImportObjectFact(fn.Object(), fact) {
return fact.Params
}
if fn.Pkg != pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA).Pkg {
return nil
}
if fn.Blocks == nil {
return nil
}
if seenFns == nil {
// impl is being called in the context of impl2, which relies
// on facts having been computed already.
return nil
}
if _, ok := seenFns[fn]; ok {
return nil
}
seenFns[fn] = struct{}{}
defer func() {
for _, v := range out {
if len(v) > 0 {
pass.ExportObjectFact(fn.Object(), &mutatesParametersFact{out})
break
}
}
}()
isMethod := fn.Signature.Recv() != nil
out = make([]mutationStack, len(fn.Params))
params := fn.Params
if isMethod {
out = out[1:]
params = params[1:]
}
seen := map[ssa.Value]struct{}{}
for i, param := range params {
if stack, ok := mutates(pass, param, seen, seenFns, nil); ok {
out[i] = stack
}
}
return out
}
type mutationStack []mutationStackEntry
type mutationStackEntry struct {
Kind string
Position token.Position
}
func newMutationStackEntry(pass *analysis.Pass, instr ssa.Instruction) mutationStackEntry {
var kind string
switch instr.(type) {
case *ssa.Store:
kind = "store"
case *ssa.IndexAddr:
kind = "pointer to slice element"
case *ssa.Slice:
kind = "slice"
case *ssa.Call:
kind = "call"
case *ssa.UnOp:
kind = "dereference"
case *ssa.FieldAddr:
kind = "field address"
case *ssa.Phi:
kind = "phi"
case *ssa.TypeAssert:
kind = "type assert"
case *ssa.MapUpdate:
kind = "map write"
case *ssa.ChangeType:
kind = "type change"
case *ssa.Lookup:
kind = "lookup"
default:
panic(fmt.Sprintf("internal error: %T", instr))
}
return mutationStackEntry{
Kind: kind,
Position: pass.Fset.Position(instr.Pos()),
}
}
func checkValue(pass *analysis.Pass, v ssa.Value) {
if ptr, ok := v.Type().(*types.Pointer); ok {
if named, ok := ptr.Elem().(*types.Named); ok {
if pass.ImportObjectFact(named.Obj(), &isImmutable{}) {
if m, ok := mutates(pass, v, map[ssa.Value]struct{}{}, nil, nil); ok {
pos := v.Pos()
if pos == token.NoPos {
switch v := v.(type) {
case *ssa.Extract:
pos = v.Tuple.Pos()
}
}
posn := pass.Fset.Position(pos)
// TODO(dh): we skip tests for now, because of absurd amounts of (false?) positives
if !strings.HasSuffix(posn.Filename, "_test.go") {
// We can't use RelatedInformation here, because we don't have token.Pos for our mutation stack, we have token.Position.
diag := analysis.Diagnostic{
Pos: v.Pos(),
Message: "this immutable value gets modified\n",
}
for _, mm := range m {
diag.Message += fmt.Sprintf("\t%s at %s\n", mm.Kind, mm.Position.String())
}
pass.Report(diag)
}
}
}
}
}
}
func checkTypes(pass *analysis.Pass) {
for _, fn := range pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA).SrcFuncs {
for _, param := range fn.Params {
checkValue(pass, param)
}
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
v, ok := instr.(ssa.Value)
if !ok {
continue
}
checkValue(pass, v)
}
}
}
}
func mutates(pass *analysis.Pass, v ssa.Value, seen map[ssa.Value]struct{}, seenFns map[*ssa.Function]struct{}, allowed map[ssa.Instruction]struct{}) (mutationStack, bool) {
if !isInterestingType(v.Type()) {
return nil, false
}
if _, ok := seen[v]; ok {
return nil, false
}
seen[v] = struct{}{}
refs := v.Referrers()
if refs == nil {
return nil, false
}
// TODO(dh): support functions that act as constructors
if alloc, ok := v.(*ssa.Alloc); ok {
// Allow struct initializers. We detect struct initializers by
// looking for stores into fields of an alloc that occur in
// the same block as the alloc.
// XXX allow assignments in more than a single block
allowed = map[ssa.Instruction]struct{}{}
for _, instr := range alloc.Block().Instrs {
switch instr := instr.(type) {
case *ssa.Store:
// XXX don't allow stores if the alloc has been used in the meantime
switch addr := instr.Addr.(type) {
case *ssa.IndexAddr:
if addr.X == alloc {
allowed[instr] = struct{}{}
}
case *ssa.FieldAddr:
if addr.X == alloc {
allowed[instr] = struct{}{}
}
}
}
}
}
for _, ref := range *refs {
if _, ok := allowed[ref]; ok {
continue
}
switch ref := ref.(type) {
case *ssa.Call:
// TODO(dh): check if the function modifies its parameter
callee := ref.Call.StaticCallee()
if callee == nil {
continue
}
isMethod := callee.Signature.Recv() != nil
args := ref.Call.Args
if isMethod {
args = args[1:]
}
for i, arg := range args {
if arg == v {
ret := markMutator(pass, callee, seenFns)
if len(ret) == 0 {
continue
}
if len(ret[i]) > 0 {
return append(mutationStack{newMutationStackEntry(pass, ref)}, ret[i]...), true
}
}
}
case *ssa.Extract, *ssa.Index:
// XXX implement
case *ssa.IndexAddr, *ssa.Lookup, *ssa.Slice, *ssa.FieldAddr:
ops := ref.Operands(nil)
if len(ops) > 0 && ops[0] != nil && *ops[0] == v {
if m, ok := mutates(pass, ref.(ssa.Value), seen, seenFns, allowed); ok {
return append(mutationStack{newMutationStackEntry(pass, ref)}, m...), true
}
}
case *ssa.UnOp:
if _, ok := ref.X.Type().Underlying().(*types.Pointer); ok && ref.Op == token.MUL {
if m, ok := mutates(pass, ref, seen, seenFns, allowed); ok {
return append(mutationStack{newMutationStackEntry(pass, ref)}, m...), true
}
}
case *ssa.Field, *ssa.MakeInterface, *ssa.ChangeType, *ssa.Phi, *ssa.Convert:
if m, ok := mutates(pass, ref.(ssa.Value), seen, seenFns, allowed); ok {
return append(mutationStack{newMutationStackEntry(pass, ref)}, m...), true
}
case *ssa.Store:
if ref.Addr == v {
return mutationStack{newMutationStackEntry(pass, ref)}, true
}
case *ssa.MapUpdate:
if ref.Map == v {
return mutationStack{newMutationStackEntry(pass, ref)}, true
}
case *ssa.Defer, *ssa.Go, *ssa.MakeClosure:
// TODO(dh): do we have to do anything here?
case *ssa.ChangeInterface:
if m, ok := mutates(pass, ref, seen, seenFns, allowed); ok {
return append(mutationStack{newMutationStackEntry(pass, ref)}, m...), true
}
case *ssa.Range:
// TODO(dh): support this
case *ssa.Return, *ssa.BinOp, *ssa.Select, *ssa.Send, *ssa.If, *ssa.Panic, *ssa.MakeSlice, *ssa.MakeChan, *ssa.MakeMap:
// do nothing
case *ssa.TypeAssert:
if m, ok := mutates(pass, ref, seen, seenFns, allowed); ok {
return append(mutationStack{newMutationStackEntry(pass, ref)}, m...), true
}
default:
panic(fmt.Sprintf("unhandled type %T", ref))
}
}
return nil, false
}
func isInterestingType(typ types.Type) bool {
switch typ.Underlying().(type) {
case *types.Struct:
case *types.Slice:
case *types.Map:
case *types.Pointer:
case *types.Interface:
default:
return false
}
return true
}
+74
View File
@@ -0,0 +1,74 @@
//go:build ignore
package pkg
//immut:type T
type T struct {
a int
b map[string]int
c *T
d []int
e int
f int
g int
h *T3
i map[string]*T
}
func (t *T) Mut() {
t.a++
}
type t2 struct {
x *int
}
type T3 struct {
x int
}
func gen() *T { return nil }
func (t *T) Mut2(t3 *T3) {
// all of these should flag, they're fairly direct mutations of T
t.a++
t.b[""] = 1
t.b[""]++
t.c.a++
t.d[2] = 1
x := &t.e
*x++
y := &t.f
if false {
y = nil
}
*y++
// should flag, because &t.g is immutable
z := t2{&t.g}
*z.x++
// should flag, because it's addressed through the immutable T
t.h.x++
// should not flag, because T3 is not immutable
t3.x++
// should flag
t.i[""].a++ // FLAGGED
// should flag, inc is known to mutate its param
inc(&t.a)
// should flag, add is known to mutate its param
add(t.b)
}
func inc(x *int) {
*x++
}
func add(m map[string]int) {
m[""] = 0
}
@@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package inconsistentReceiverName
import (
"go/ast"
"strings"
"golang.org/x/tools/go/analysis"
)
var (
Analyzer = &analysis.Analyzer{
Name: "inconsistentReceiverName",
Doc: "check for inconsistent receiver names in the methods of a struct",
Run: run,
}
ignoreFilesPattern string
)
type firstReceiverName = struct {
Name string
Node ast.Node
}
func init() {
Analyzer.Flags.StringVar(&ignoreFilesPattern, "ignore", "", "Comma separated list of files to ignore")
}
func run(pass *analysis.Pass) (interface{}, error) {
var ignoreFiles []string
if ignoreFilesPattern != "" {
ignoreFiles = strings.Split(ignoreFilesPattern, ",")
}
firstReceiverNameForType := make(map[string]firstReceiverName)
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
if node != nil {
f := pass.Fset.File(node.Pos())
if isIgnore(f.Name(), ignoreFiles) {
return false
}
}
switch funDecl := node.(type) {
case *ast.FuncDecl:
if funDecl.Recv != nil && len(funDecl.Recv.List) > 0 && len(funDecl.Recv.List[0].Names) > 0 {
field := funDecl.Recv.List[0]
expr := field.Type
starExpr, ok := field.Type.(*ast.StarExpr)
if ok {
expr = starExpr.X
}
typeIdent, ok := expr.(*ast.Ident)
if !ok {
return false
}
currentReceiverName := field.Names[0].Name
firstReceiver, ok := firstReceiverNameForType[typeIdent.Name]
if !ok {
firstReceiverNameForType[typeIdent.Name] = firstReceiverName{
Name: currentReceiverName,
Node: node,
}
return true
}
if firstReceiver.Name != currentReceiverName {
otherReceiverName := firstReceiver.Name
otherReceiverPosition := pass.Fset.Position(firstReceiver.Node.Pos())
pass.Reportf(
node.Pos(),
"Different receiver name used for the struct \"%s\" in different methods: using \"%s\" here but was named as \"%s\" before at %s",
typeIdent.Name,
currentReceiverName,
otherReceiverName,
otherReceiverPosition.String(),
)
}
}
}
return true
})
}
return nil, nil
}
func isIgnore(file string, ignoreFiles []string) bool {
for _, f := range ignoreFiles {
if strings.HasSuffix(file, f) {
return true
}
}
return false
}
@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package inconsistentReceiverName
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAll(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package a
// Valid: consistent receiver names
type ValidServer struct {
name string
}
func (s *ValidServer) Start() {
}
func (s *ValidServer) Stop() {
}
func (s *ValidServer) GetName() string {
return s.name
}
// Invalid: inconsistent receiver names
type InvalidServer struct {
name string
}
func (s *InvalidServer) Start() {
}
func (srv *InvalidServer) Stop() { // want "Different receiver name used for the struct \"InvalidServer\" in different methods"
}
func (server *InvalidServer) GetName() string { // want "Different receiver name used for the struct \"InvalidServer\" in different methods"
return server.name
}
// Valid: different structs can use different receiver names
type Client struct{}
func (c *Client) Connect() {
}
func (c *Client) Disconnect() {
}
// Invalid: more than two different receiver names
type Database struct{}
func (d *Database) Open() {
}
func (db *Database) Close() { // want "Different receiver name used for the struct \"Database\" in different methods"
}
func (database *Database) Query() { // want "Different receiver name used for the struct \"Database\" in different methods"
}
// Valid: non-pointer receivers
type Counter struct {
count int
}
func (c Counter) Increment() Counter {
c.count++
return c
}
func (c Counter) Get() int {
return c.count
}
+180
View File
@@ -0,0 +1,180 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package license
import (
"fmt"
"go/ast"
"regexp"
"strings"
"time"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "license",
Doc: "check the license header.",
Run: run,
}
var EEAnalyzer = &analysis.Analyzer{
Name: "enterpriseLicense",
Doc: "check the license header for enterprise code.",
Run: run,
}
const defaultLicenseYear = 2015
var (
ignoreFilesPattern string
licenseYear int
sourceAvailablePackageRe = regexp.MustCompile("/enterprise")
)
const (
licenseLine1Format = "// Copyright (c) %d-present Mattermost, Inc. All Rights Reserved."
defaultLicenseLine2 = "// See LICENSE.txt for license information."
enterpriseLicenseLine2 = "// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information."
sourceAvailableLicenseLine2 = "// See LICENSE.enterprise for license information."
)
// buildHeaders are allowed to precede the license headers.
var buildHeaders = []string{
"//go:generate",
"//go:build",
}
// generatedHeaders indicate files to automatically ignore.
var generatedHeaders = []string{
"// Code generated by mockery",
"// Code generated by MockGen. DO NOT EDIT.",
"by go-bindata DO NOT EDIT. (@generated)",
"// This file is automatically generated. Do not modify it manually.",
}
func init() {
Analyzer.Flags.StringVar(&ignoreFilesPattern, "ignore", "", "Comma separated list of files to ignore")
Analyzer.Flags.IntVar(&licenseYear, "year", defaultLicenseYear, "Year to use in license header (2015-present)")
EEAnalyzer.Flags.StringVar(&ignoreFilesPattern, "ignore", "", "Comma separated list of files to ignore")
EEAnalyzer.Flags.IntVar(&licenseYear, "year", defaultLicenseYear, "Year to use in license header (2015-present)")
}
func run(pass *analysis.Pass) (interface{}, error) {
var ignoreFiles []string
if ignoreFilesPattern != "" {
ignoreFiles = strings.Split(ignoreFilesPattern, ",")
}
// Validate license year
if licenseYear == 0 {
licenseYear = defaultLicenseYear
}
currentYear := time.Now().Year()
if licenseYear < defaultLicenseYear || licenseYear > currentYear {
return nil, fmt.Errorf("license year must be between %d and %d", defaultLicenseYear, currentYear)
}
expectedLine1 := fmt.Sprintf(licenseLine1Format, licenseYear)
expectedLine2 := defaultLicenseLine2
if pass.Analyzer.Name == "enterpriseLicense" {
// Closed source enterprise license
expectedLine2 = enterpriseLicenseLine2
} else if sourceAvailablePackageRe.MatchString(pass.Pkg.Path()) {
// Source available license
expectedLine2 = sourceAvailableLicenseLine2
}
for _, file := range pass.Files {
checkFile(pass, file, ignoreFiles, expectedLine1, expectedLine2)
}
return nil, nil
}
func checkFile(pass *analysis.Pass, file *ast.File, ignoreFiles []string, expectedLine1 string, expectedLine2 string) {
fileObj := pass.Fset.File(file.Pos())
// Ignore files specified via -ignore
if fileIsIgnored(fileObj.Name(), ignoreFiles) {
return
}
if len(file.Comments) == 0 {
pass.Reportf(file.Pos(), "License not found")
return
}
if len(file.Comments[0].List) == 0 {
pass.Reportf(file.Pos(), "License not found or wrong")
return
}
// Ignore known generated files.
for _, header := range generatedHeaders {
if strings.Contains(file.Comments[0].List[0].Text, header) {
return
}
}
// Allow the first comment block to be entirely build directives. If found,
// the next comment block will be required to be a license and must naturally
// be separated by an empty line (otherwise it would be the same comment block).
commentGroup := 0
foundBuildHeader := false
allBuildHeaders := true
for _, commentText := range file.Comments[commentGroup].List {
isBuildHeader := false
for _, buildHeader := range buildHeaders {
if strings.HasPrefix(commentText.Text, buildHeader) {
isBuildHeader = true
break
}
}
if isBuildHeader {
foundBuildHeader = true
} else {
allBuildHeaders = false
}
}
if foundBuildHeader && !allBuildHeaders {
pass.Reportf(file.Pos(), "Must be an empty line between the build directive and the license")
return
} else if foundBuildHeader && allBuildHeaders {
commentGroup++
}
if len(file.Comments) < commentGroup+1 {
pass.Reportf(file.Pos(), "License not found")
return
}
if len(file.Comments[commentGroup].List) < 2 {
pass.Reportf(file.Pos(), "License not found or wrong")
return
}
if file.Comments[commentGroup].List[0].Text != expectedLine1 {
pass.Reportf(file.Comments[commentGroup].List[0].Pos(), "License copywright wrong, expected: %s", expectedLine1)
return
}
if file.Comments[commentGroup].List[1].Text != expectedLine2 {
pass.Reportf(file.Comments[commentGroup].List[1].Pos(), "License reference wrong, expected %s", expectedLine2)
return
}
}
func fileIsIgnored(file string, ignoreFiles []string) bool {
for _, f := range ignoreFiles {
if strings.HasSuffix(file, f) {
return true
}
}
return false
}
@@ -0,0 +1,202 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package license_test
import (
"fmt"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/tools/mattermost-govet/license"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/analysistest"
)
type MockT struct {
calls []string
}
func (mt *MockT) Errorf(format string, args ...interface{}) {
mt.calls = append(mt.calls, fmt.Sprintf(format, args...))
}
func TestLicense(t *testing.T) {
testCases := []struct {
Description string
Path string
Analyzer *analysis.Analyzer
}{
{
"Standard",
"standard",
license.Analyzer,
},
{
"Enterprise",
"enterprise",
license.EEAnalyzer,
},
{
"Source Available",
"source_available/enterprise",
license.Analyzer,
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
t.Run("ignored", func(t *testing.T) {
t.Run("specified file", func(t *testing.T) {
testdata := analysistest.TestData()
require.NoError(t, testCase.Analyzer.Flags.Set("ignore", "ignored1.go,ignored2.go"))
t.Cleanup(func() {
require.NoError(t, testCase.Analyzer.Flags.Set("ignore", ""))
})
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "ignored_files"))
})
t.Run("mockery", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "ignored/mockery"))
})
t.Run("mockgen", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "ignored/mockgen"))
})
t.Run("go-bindata", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "ignored/go-bin-data"))
})
t.Run("manifest", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "ignored/manifest"))
})
})
t.Run("missing license", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "missing"))
})
t.Run("valid license", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "valid"))
})
t.Run("invalid copyright on line 1", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "invalid_copyright"))
})
t.Run("invalid reference on line 2", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "invalid_reference"))
})
t.Run("parameterized year", func(t *testing.T) {
t.Run("invalid year", func(t *testing.T) {
mt := &MockT{}
testdata := analysistest.TestData()
require.NoError(t, testCase.Analyzer.Flags.Set("year", "-1"))
t.Cleanup(func() {
require.NoError(t, testCase.Analyzer.Flags.Set("year", "0"))
})
analysistest.Run(mt, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "valid"))
require.Len(t, mt.calls, 1)
for _, call := range mt.calls {
require.Contains(t, call, "license year must be between 2015 and")
}
})
t.Run("before 2015", func(t *testing.T) {
mt := &MockT{}
testdata := analysistest.TestData()
require.NoError(t, testCase.Analyzer.Flags.Set("year", "2014"))
t.Cleanup(func() {
require.NoError(t, testCase.Analyzer.Flags.Set("year", "0"))
})
analysistest.Run(mt, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "valid"))
require.Len(t, mt.calls, 1)
for _, call := range mt.calls {
require.Contains(t, call, "license year must be between 2015 and")
}
})
t.Run("2015", func(t *testing.T) {
testdata := analysistest.TestData()
require.NoError(t, testCase.Analyzer.Flags.Set("year", "2015"))
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "parameterized_year/2015"))
})
t.Run("2024", func(t *testing.T) {
testdata := analysistest.TestData()
require.NoError(t, testCase.Analyzer.Flags.Set("year", "2024"))
t.Cleanup(func() {
require.NoError(t, testCase.Analyzer.Flags.Set("year", "0"))
})
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "parameterized_year/2024"))
})
t.Run("current year", func(t *testing.T) {
testdata := analysistest.TestData()
require.NoError(t, testCase.Analyzer.Flags.Set("year", "2026"))
t.Cleanup(func() {
require.NoError(t, testCase.Analyzer.Flags.Set("year", "0"))
})
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "parameterized_year/current"))
})
t.Run("future year", func(t *testing.T) {
mt := &MockT{}
testdata := analysistest.TestData()
require.NoError(t, testCase.Analyzer.Flags.Set("year", "2027"))
t.Cleanup(func() {
require.NoError(t, testCase.Analyzer.Flags.Set("year", "0"))
})
analysistest.Run(mt, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "valid"))
require.Len(t, mt.calls, 1)
for _, call := range mt.calls {
require.Contains(t, call, "license year must be between 2015 and")
}
})
})
t.Run("build directives", func(t *testing.T) {
t.Run("go:generate with valid license", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "build/gogenerate"))
})
t.Run("go:generate with invalid license", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "build/gogenerate_invalid"))
})
t.Run("go:build with valid license", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "build/buildtag"))
})
t.Run("directive with valid license but without newline", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "build/withoutnewline"))
})
t.Run("multiple build directives with valid license", func(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, testCase.Analyzer, filepath.Join(testCase.Path, "build/multipledirectives"))
})
})
})
}
}
@@ -0,0 +1,8 @@
//go:build !blah
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package buildtag
func Build() {}
@@ -0,0 +1,8 @@
//go:generate echo "generating"
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package gogenerate
func Generate() {}
@@ -0,0 +1,8 @@
//go:generate
// Wrong copyright line // want "License copywright wrong, expected: // Copyright \\(c\\) 2015-present Mattermost, Inc\\. All Rights Reserved\\."
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package gogenerate_invalid
func Generate() {}
@@ -0,0 +1,9 @@
//go:generate echo "command1"
//go:generate echo "command2"
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package multipledirectives
func Build() {}
@@ -0,0 +1,7 @@
//go:build !blah
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package withoutnewline // want "Must be an empty line between the build directive and the license"
func Build() {}
@@ -0,0 +1,5 @@
// Code generated by go-bindata DO NOT EDIT. (@generated)
package bindata
func Bindata() {}
@@ -0,0 +1,5 @@
// This file is automatically generated. Do not modify it manually.
package manifest
func Manifest() {}
@@ -0,0 +1,5 @@
// Code generated by mockery
package mockery
func Mock() {}
@@ -0,0 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
package mockgen
func Mock() {}
@@ -0,0 +1,3 @@
package ignored_files
func Foo() {}
@@ -0,0 +1,3 @@
package ignored_files
func Bar() {}
@@ -0,0 +1,6 @@
// Wrong copyright line // want `License copywright wrong, expected: // Copyright \(c\) 2015-present Mattermost, Inc\. All Rights Reserved\.`
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package invalid_copyright
func Invalid() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// Wrong license reference // want `License reference wrong, expected // See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.`
package invalid_reference
func Invalid() {}
@@ -0,0 +1,3 @@
package missing // want `License not found or wrong`
func Missing() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package year2015
func Test() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2024-present Mattermost, Inc. All Rights Reserved.
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package year2024
func Test() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2026-present Mattermost, Inc. All Rights Reserved.
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package yearcurrent
func Test() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See ENTERPRISE-LICENSE.txt and SOURCE-CODE-LICENSE.txt for license information.
package valid
func Valid() {}
@@ -0,0 +1,8 @@
//go:build !blah
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package buildtag
func Build() {}
@@ -0,0 +1,8 @@
//go:generate echo "generating"
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package gogenerate
func Generate() {}
@@ -0,0 +1,8 @@
//go:generate
// Wrong copyright line // want "License copywright wrong, expected: // Copyright \\(c\\) 2015-present Mattermost, Inc\\. All Rights Reserved\\."
// See LICENSE.enterprise for license information.
package gogenerate_invalid
func Generate() {}
@@ -0,0 +1,9 @@
//go:generate echo "command1"
//go:generate echo "command2"
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package multipledirectives
func Build() {}
@@ -0,0 +1,7 @@
//go:build !blah
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package withoutnewline // want "Must be an empty line between the build directive and the license"
func Build() {}
@@ -0,0 +1,5 @@
// Code generated by go-bindata DO NOT EDIT. (@generated)
package bindata
func Bindata() {}
@@ -0,0 +1,5 @@
// This file is automatically generated. Do not modify it manually.
package manifest
func Manifest() {}
@@ -0,0 +1,5 @@
// Code generated by mockery
package mockery
func Mock() {}
@@ -0,0 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
package mockgen
func Mock() {}
@@ -0,0 +1,3 @@
package ignored_files
func Foo() {}
@@ -0,0 +1,3 @@
package ignored_files
func Bar() {}
@@ -0,0 +1,6 @@
// Wrong copyright line // want `License copywright wrong, expected: // Copyright \(c\) 2015-present Mattermost, Inc\. All Rights Reserved\.`
// See LICENSE.enterprise for license information.
package invalid_copyright
func Invalid() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// Wrong license reference // want `License reference wrong, expected // See LICENSE.enterprise for license information.`
package invalid_reference
func Invalid() {}
@@ -0,0 +1,3 @@
package missing // want `License not found or wrong`
func Missing() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package year2015
func Test() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2024-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package year2024
func Test() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2026-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package yearcurrent
func Test() {}
@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package valid
func Valid() {}
@@ -0,0 +1,8 @@
//go:build !blah
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package buildtag
func Build() {}
@@ -0,0 +1,8 @@
//go:generate echo "generating"
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package gogenerate
func Generate() {}
@@ -0,0 +1,8 @@
//go:generate
// Wrong copyright line // want "License copywright wrong, expected: // Copyright \\(c\\) 2015-present Mattermost, Inc\\. All Rights Reserved\\."
// See LICENSE.txt for license information.
package gogenerate_invalid
func Generate() {}
@@ -0,0 +1,9 @@
//go:generate echo "command1"
//go:generate echo "command2"
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package multipledirectives
func Build() {}
@@ -0,0 +1,7 @@
//go:build !blah
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package withoutnewline // want "Must be an empty line between the build directive and the license"
func Build() {}

Some files were not shown because too many files have changed in this diff Show More