mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
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:
committed by
GitHub
co-authored by
Mattermost Build
parent
083681f8ca
commit
a87917360a
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
exclusions:
|
||||
rules:
|
||||
- path: immut/testdata.go
|
||||
linters:
|
||||
- govet
|
||||
text: "buildtag"
|
||||
@@ -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
|
||||
@@ -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;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
CREATE INDEX
|
||||
IF NOT EXISTS idx_multiline
|
||||
ON foo (bar);
|
||||
DROP INDEX CONCURRENTLY IF EXISTS idx_multiline_ok;
|
||||
+1
@@ -0,0 +1 @@
|
||||
CREATE INDEX CONCURRENTLY idx_safe ON t (c); CREATE INDEX idx_unsafe ON t (d);
|
||||
+2
@@ -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")
|
||||
}
|
||||
@@ -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{}) {}
|
||||
+51
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+26
@@ -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
|
||||
}
|
||||
+26
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+8
@@ -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() {}
|
||||
+8
@@ -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() {}
|
||||
Vendored
+8
@@ -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() {}
|
||||
+9
@@ -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() {}
|
||||
+7
@@ -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() {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Code generated by go-bindata DO NOT EDIT. (@generated)
|
||||
|
||||
package bindata
|
||||
|
||||
func Bindata() {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// This file is automatically generated. Do not modify it manually.
|
||||
|
||||
package manifest
|
||||
|
||||
func Manifest() {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Code generated by mockery
|
||||
|
||||
package mockery
|
||||
|
||||
func Mock() {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
|
||||
package mockgen
|
||||
|
||||
func Mock() {}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package ignored_files
|
||||
|
||||
func Foo() {}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package ignored_files
|
||||
|
||||
func Bar() {}
|
||||
+6
@@ -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() {}
|
||||
+6
@@ -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() {}
|
||||
+6
@@ -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() {}
|
||||
+6
@@ -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() {}
|
||||
+6
@@ -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() {}
|
||||
Vendored
+8
@@ -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() {}
|
||||
tools/mattermost-govet/license/testdata/src/source_available/enterprise/build/gogenerate/generate.go
Vendored
+8
@@ -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() {}
|
||||
+8
@@ -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() {}
|
||||
+9
@@ -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() {}
|
||||
+7
@@ -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() {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Code generated by go-bindata DO NOT EDIT. (@generated)
|
||||
|
||||
package bindata
|
||||
|
||||
func Bindata() {}
|
||||
tools/mattermost-govet/license/testdata/src/source_available/enterprise/ignored/manifest/manifest.go
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
// This file is automatically generated. Do not modify it manually.
|
||||
|
||||
package manifest
|
||||
|
||||
func Manifest() {}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
// Code generated by mockery
|
||||
|
||||
package mockery
|
||||
|
||||
func Mock() {}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
|
||||
package mockgen
|
||||
|
||||
func Mock() {}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
package ignored_files
|
||||
|
||||
func Foo() {}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
package ignored_files
|
||||
|
||||
func Bar() {}
|
||||
tools/mattermost-govet/license/testdata/src/source_available/enterprise/invalid_copyright/invalid.go
Vendored
+6
@@ -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() {}
|
||||
tools/mattermost-govet/license/testdata/src/source_available/enterprise/invalid_reference/invalid.go
Vendored
+6
@@ -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() {}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package missing // want `License not found or wrong`
|
||||
|
||||
func Missing() {}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package year2015
|
||||
|
||||
func Test() {}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2024-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package year2024
|
||||
|
||||
func Test() {}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2026-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package yearcurrent
|
||||
|
||||
func Test() {}
|
||||
+6
@@ -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() {}
|
||||
+8
@@ -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() {}
|
||||
+8
@@ -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() {}
|
||||
+9
@@ -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() {}
|
||||
+7
@@ -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
Reference in New Issue
Block a user