// 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 }