Skip to content

Commit

Permalink
Add support for .gometalinterrc (alecthomas#407)
Browse files Browse the repository at this point in the history
  • Loading branch information
rliebz authored and alecthomas committed Dec 12, 2017
1 parent b8b1f84 commit 2ecd20c
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 23 deletions.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,11 @@ Additional linters can be added through the command line with `--linter=NAME:COM

## Configuration file

gometalinter now supports a JSON configuration file which can be loaded via
`--config=<file>`. The format of this file is determined by the `Config` struct
in [config.go](https://github.com/alecthomas/gometalinter/blob/master/config.go).
gometalinter now supports a JSON configuration file called `.gometalinter.json` that can
be placed at the root of your project. The configuration file will be automatically loaded
from the working directory or any parent directory and can be overridden by passing
`--config=<file>` or ignored with `--no-config`. The format of this file is determined by
the `Config` struct in [config.go](https://github.com/alecthomas/gometalinter/blob/master/config.go).

The configuration file mostly corresponds to command-line flags, with the following exceptions:

Expand All @@ -110,6 +112,12 @@ Here is an example configuration file:
}
```

If a `.gometalinter.json` file is loaded, individual options can still be overridden by
passing command-line flags. All flags are parsed in order, meaning configuration passed
with the `--config` flag will override any command-line flags passed before and be
overridden by flags passed after.


#### `Format` key

The default `Format` key places the different fields of an `Issue` into a template. this
Expand Down
53 changes: 53 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"text/template"
"time"
Expand Down Expand Up @@ -137,3 +139,54 @@ var config = &Config{
Sort: []string{"none"},
Deadline: jsonDuration(time.Second * 30),
}

func loadConfigFile(filename string) error {
r, err := os.Open(filename)
if err != nil {
return err
}
defer r.Close() // nolint: errcheck
err = json.NewDecoder(r).Decode(config)
if err != nil {
return err
}
for _, disable := range config.Disable {
for i, enable := range config.Enable {
if enable == disable {
config.Enable = append(config.Enable[:i], config.Enable[i+1:]...)
break
}
}
}
return err
}

func findDefaultConfigFile() (fullPath string, found bool, err error) {
prevPath := ""
dirPath, err := os.Getwd()
if err != nil {
return "", false, err
}

for dirPath != prevPath {
fullPath, found, err = findConfigFileInDir(dirPath)
if err != nil || found {
return fullPath, found, err
}
prevPath, dirPath = dirPath, filepath.Dir(dirPath)
}

return "", false, nil
}

func findConfigFileInDir(dirPath string) (fullPath string, found bool, err error) {
fullPath = filepath.Join(dirPath, defaultConfigPath)
if _, err := os.Stat(fullPath); err != nil {
if os.IsNotExist(err) {
return "", false, nil
}
return "", false, err
}

return fullPath, true, nil
}
61 changes: 61 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -19,3 +21,62 @@ func TestLinterConfigUnmarshalJSON(t *testing.T) {
assert.Equal(t, "/bin/custom", config.Command)
assert.Equal(t, functionName(partitionPathsAsDirectories), functionName(config.PartitionStrategy))
}

func TestFindDefaultConfigFile(t *testing.T) {
tmpdir, cleanup := setupTempDir(t)
defer cleanup()

mkDir(t, tmpdir, "contains")
mkDir(t, tmpdir, "contains", "foo")
mkDir(t, tmpdir, "contains", "foo", "bar")
mkDir(t, tmpdir, "contains", "double")
mkDir(t, tmpdir, "lacks")

mkFile(t, filepath.Join(tmpdir, "contains"), defaultConfigPath, "{}")
mkFile(t, filepath.Join(tmpdir, "contains", "double"), defaultConfigPath, "{}")

var testcases = []struct {
dir string
expected string
found bool
}{
{
dir: tmpdir,
expected: "",
found: false,
},
{
dir: filepath.Join(tmpdir, "contains"),
expected: filepath.Join(tmpdir, "contains", defaultConfigPath),
found: true,
},
{
dir: filepath.Join(tmpdir, "contains", "foo"),
expected: filepath.Join(tmpdir, "contains", defaultConfigPath),
found: true,
},
{
dir: filepath.Join(tmpdir, "contains", "foo", "bar"),
expected: filepath.Join(tmpdir, "contains", defaultConfigPath),
found: true,
},
{
dir: filepath.Join(tmpdir, "contains", "double"),
expected: filepath.Join(tmpdir, "contains", "double", defaultConfigPath),
found: true,
},
{
dir: filepath.Join(tmpdir, "lacks"),
expected: "",
found: false,
},
}

for _, testcase := range testcases {
require.NoError(t, os.Chdir(testcase.dir))
configFile, found, err := findDefaultConfigFile()
assert.Equal(t, testcase.expected, configFile)
assert.Equal(t, testcase.found, found)
assert.NoError(t, err)
}
}
39 changes: 22 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ var (
{"github.com", "alecthomas", "gometalinter", "_linters"},
{"gopkg.in", "alecthomas", "gometalinter.v2", "_linters"},
}
Version = "master"
defaultConfigPath = ".gometalinter.json"
Version = "master"
)

func setupFlags(app *kingpin.Application) {
app.Flag("config", "Load JSON configuration from file.").Envar("GOMETALINTER_CONFIG").Action(loadConfig).String()
app.Flag("no-config", "Disable automatic loading of config file.").Bool()
app.Flag("disable", "Disable previously enabled linters.").PlaceHolder("LINTER").Short('D').Action(disableAction).Strings()
app.Flag("enable", "Enable previously disabled linters.").PlaceHolder("LINTER").Short('E').Action(enableAction).Strings()
app.Flag("linter", "Define a linter.").PlaceHolder("NAME:COMMAND:PATTERN").Action(cliLinterOverrides).StringMap()
Expand Down Expand Up @@ -83,25 +85,27 @@ func cliLinterOverrides(app *kingpin.Application, element *kingpin.ParseElement,
return nil
}

func loadConfig(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
r, err := os.Open(*element.Value)
if err != nil {
return err
}
defer r.Close() // nolint: errcheck
err = json.NewDecoder(r).Decode(config)
if err != nil {
return err
func loadDefaultConfig(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
if element != nil {
return nil
}
for _, disable := range config.Disable {
for i, enable := range config.Enable {
if enable == disable {
config.Enable = append(config.Enable[:i], config.Enable[i+1:]...)
break
}

for _, elem := range ctx.Elements {
if f := elem.OneOf.Flag; f == app.GetFlag("config") || f == app.GetFlag("no-config") {
return nil
}
}
return err

configFile, found, err := findDefaultConfigFile()
if err != nil || !found {
return err
}

return loadConfigFile(configFile)
}

func loadConfig(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
return loadConfigFile(*element.Value)
}

func disableAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
Expand Down Expand Up @@ -176,6 +180,7 @@ func main() {
kingpin.Version(Version)
pathsArg := kingpin.Arg("path", "Directories to lint. Defaults to \".\". <path>/... will recurse.").Strings()
app := kingpin.CommandLine
app.Action(loadDefaultConfig)
setupFlags(app)
app.Help = fmt.Sprintf(`Aggregate and normalise the output of a whole bunch of Go linters.
Expand Down
68 changes: 65 additions & 3 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func setupTempDir(t *testing.T) (string, func()) {
tmpdir, err := ioutil.TempDir("", "test-expand-paths")
require.NoError(t, err)

tmpdir, err = filepath.EvalSymlinks(tmpdir)
require.NoError(t, err)

oldwd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(tmpdir))
Expand All @@ -100,12 +103,15 @@ func mkDir(t *testing.T, paths ...string) {
mkGoFile(t, fullPath, "file.go")
}

func mkGoFile(t *testing.T, path string, filename string) {
content := []byte("package foo")
err := ioutil.WriteFile(filepath.Join(path, filename), content, 0644)
func mkFile(t *testing.T, path string, filename string, content string) {
err := ioutil.WriteFile(filepath.Join(path, filename), []byte(content), 0644)
require.NoError(t, err)
}

func mkGoFile(t *testing.T, path string, filename string) {
mkFile(t, path, filename, "package foo")
}

func TestPathFilter(t *testing.T) {
skip := []string{"exclude", "skip.go"}
pathFilter := newPathFilter(skip)
Expand All @@ -129,6 +135,62 @@ func TestPathFilter(t *testing.T) {
}
}

func TestLoadDefaultConfig(t *testing.T) {
originalConfig := *config
defer func() { config = &originalConfig }()

tmpdir, cleanup := setupTempDir(t)
defer cleanup()

mkFile(t, tmpdir, defaultConfigPath, `{"Deadline": "3m"}`)

app := kingpin.New("test-app", "")
app.Action(loadDefaultConfig)
setupFlags(app)

_, err := app.Parse([]string{})
require.NoError(t, err)
require.Equal(t, 3*time.Minute, config.Deadline.Duration())
}

func TestNoConfigFlag(t *testing.T) {
originalConfig := *config
defer func() { config = &originalConfig }()

tmpdir, cleanup := setupTempDir(t)
defer cleanup()

mkFile(t, tmpdir, defaultConfigPath, `{"Deadline": "3m"}`)

app := kingpin.New("test-app", "")
app.Action(loadDefaultConfig)
setupFlags(app)

_, err := app.Parse([]string{"--no-config"})
require.NoError(t, err)
require.Equal(t, 30*time.Second, config.Deadline.Duration())
}

func TestConfigFlagSkipsDefault(t *testing.T) {
originalConfig := *config
defer func() { config = &originalConfig }()

tmpdir, cleanup := setupTempDir(t)
defer cleanup()

mkFile(t, tmpdir, defaultConfigPath, `{"Deadline": "3m"}`)
mkFile(t, tmpdir, "test-config", `{"Fast": true}`)

app := kingpin.New("test-app", "")
app.Action(loadDefaultConfig)
setupFlags(app)

_, err := app.Parse([]string{"--config", filepath.Join(tmpdir, "test-config")})
require.NoError(t, err)
require.Equal(t, 30*time.Second, config.Deadline.Duration())
require.Equal(t, true, config.Fast)
}

func TestLoadConfigWithDeadline(t *testing.T) {
originalConfig := *config
defer func() { config = &originalConfig }()
Expand Down

0 comments on commit 2ecd20c

Please sign in to comment.