dev-tools/mage/linter.go (108 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package mage import ( "errors" "fmt" "io" "log" "net/http" "os" "path/filepath" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" ) const ( linterVersion = "v2.0.2" linterInstallURL = "https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh" ) var ( linterConfigFilename = filepath.Join(".", ".golangci.yml") linterInstallDir = filepath.Join(".", "build") linterInstallFile = filepath.Join(linterInstallDir, "install-golang-ci.sh") linterBinaryFile = filepath.Join(linterInstallDir, linterVersion, "golangci-lint") ) // Linter contains targets related to linting the Go code type Linter mg.Namespace // CheckConfig makes sure that the `.golangci.yml` does not have uncommitted changes func (Linter) CheckConfig() error { err := assertUnchanged(linterConfigFilename) if err != nil { return fmt.Errorf("linter configuration has uncommitted changes: %w", err) } return nil } // Install installs golangci-lint (https://golangci-lint.run) to `./build` // using the official installation script downloaded from GitHub. // If the linter binary already exists does nothing. func (Linter) Install() error { return install(false) } // ForceInstall force installs the linter regardless of whether it exists or not. func (Linter) ForceInstall() error { return install(true) } func install(force bool) error { dirPath := filepath.Dir(linterBinaryFile) err := os.MkdirAll(dirPath, 0700) if err != nil { return fmt.Errorf("failed to create path %q: %w", dirPath, err) } _, err = os.Stat(linterBinaryFile) if !force && err == nil { log.Println("The linter has been already installed, skipping...") return nil } if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed check if file %q exists: %w", linterBinaryFile, err) } log.Println("Preparing the installation script file...") installScript, err := os.OpenFile(linterInstallFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0700) if err != nil { return fmt.Errorf("failed to create file %q: %w", linterInstallFile, err) } defer installScript.Close() log.Println("Downloading the linter installation script...") //nolint:noctx // valid use since there is no context resp, err := http.Get(linterInstallURL) if err != nil { return fmt.Errorf("cannot download the linter installation script from %q: %w", linterInstallURL, err) } defer resp.Body.Close() lr := io.LimitReader(resp.Body, 1024*100) // not more than 100 KB, just to be safe _, err = io.Copy(installScript, lr) if err != nil { return fmt.Errorf("failed to finish downloading the linter installation script: %w", err) } err = installScript.Close() // otherwise we cannot run the script if err != nil { return fmt.Errorf("failed to close file %q: %w", linterInstallFile, err) } binaryDir := filepath.Dir(linterBinaryFile) err = os.MkdirAll(binaryDir, 0700) if err != nil { return fmt.Errorf("cannot create path %q: %w", binaryDir, err) } // there must be no space after `-b`, otherwise the script does not work correctly ¯\_(ツ)_/¯ return sh.Run(linterInstallFile, "-b"+binaryDir, linterVersion) } // All runs the linter against the entire codebase func (l Linter) All() error { mg.Deps(l.Install, l.CheckConfig) return runLinter() } // Prints the version of the linter in use. func (l Linter) Version() error { mg.Deps(l.Install) return runLinter("--version") } // LastChange runs the linter against all files changed since the fork point from `main`. // If the current branch is `main` then runs against the files changed in the last commit. func (l Linter) LastChange() error { mg.Deps(l.Install, l.CheckConfig) // get current branch name branch, err := sh.Output("git", "rev-parse", "--abbrev-ref", "HEAD") if err != nil { return fmt.Errorf("failed to get the current branch: %w", err) } // the linter is supposed to support linting changed diffs only but, // for some reason, it simply does not work - does not output any // results without linting the whole files, so we have to use `--whole-files` // which can lead to some frustration from developers who would like to // fix a single line in an existing codebase and the linter would force them // into fixing all linting issues in the whole file instead if branch == "main" { // files changed in the last commit return runLinter("--new-from-rev=HEAD~", "--whole-files") } return runLinter("--new-from-rev=origin/main", "--whole-files") } // runLinter runs the linter passing the `mage -v` (verbose mode) and given arguments. // Also redirects linter's output to the `stderr` instead of discarding it. func runLinter(runFlags ...string) error { var args []string if mg.Verbose() { args = append(args, "-v") } args = append(args, "run") args = append(args, runFlags...) args = append(args, "-c", linterConfigFilename) args = append(args, "./...") return runWithStdErr(linterBinaryFile, args...) }