Repository: alecthomas/gometalinter
Pull request #352 (2017-09-17 23:30:54)

Support setting of all linter config fields from the config file
Adds support for setting `IsFast` and `PartitionStrategy` from the config file. Technically `InstallFrom` can be set, but it doesn't do anything just yet because `--install` always reads the defaultLinters instead of any config.

Updates the README to document linter settings in the configuration file.

Also adds a `single-directory` partition strategy that might allow for a workaround for #351

dnephin: Maybe we should change the name `PartitionStrategy` to something like `PathArgs` ? 
alecthomas: Ah conflicts.
dnephin: rebased and fixed conflicts, will merge if build is green

Patch:
diff --git a/README.md b/README.md
index fc177ad..08b6dac 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,6 @@
     - [2. Analyse the debug output](#2-analyse-the-debug-output)
     - [3. Report an issue.](#3-report-an-issue)
   - [How do I filter issues between two git refs?](#how-do-i-filter-issues-between-two-git-refs)
-- [Details](#details)
 - [Checkstyle XML format](#checkstyle-xml-format)
 
 <!-- /MarkdownTOC -->
@@ -91,8 +90,8 @@ 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`.
+`--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).
 
 The configuration file mostly corresponds to command-line flags, with the following exceptions:
 
@@ -108,6 +107,34 @@ Here is an example configuration file:
 }
 ```
 
+### Adding Custom linters
+
+Linters can be added and customized from the config file using the `Linters` field.
+Linters supports the following fields:
+
+* `Command` - the path to the linter binary and any default arguments
+* `Pattern` - a regular expression used to parse the linter output
+* `IsFast` - if the linter should be run when the `--fast` flag is used
+* `PartitionStrategy` - how paths args should be passed to the linter command:
+  * `directories` - call the linter once with a list of all the directories
+  * `files` - call the linter once with a list of all the files
+  * `packages` - call the linter once with a list of all the package paths
+  * `files-by-package` - call the linter once per package with a list of the
+    files in the package.
+  * `single-directory` - call the linter once per directory
+
+The config for default linters can be overridden by using the name of the
+linter.
+
+Additional linters can be configured via the command line using the format
+`NAME:COMMAND:PATTERN`.
+
+Example:
+
+```
+$ gometalinter --linter='vet:go tool vet -printfuncs=Infof,Debugf,Warningf,Errorf:PATH:LINE:MESSAGE' .
+```
+
 ## Installing
 
 There are two options for installing gometalinter.
@@ -308,21 +335,6 @@ gometalinter |& revgrep master        # Show issues between master and HEAD (or
 gometalinter |& revgrep origin/master # Show issues that haven't been pushed.
 ```
 
-## Details
-
-Additional linters can be configured via the command line:
-
-```
-$ gometalinter --linter='vet:go tool vet -printfuncs=Infof,Debugf,Warningf,Errorf:PATH:LINE:MESSAGE' .
-stutter.go:21:15:warning: error return value not checked (defer a.Close()) (errcheck)
-stutter.go:22:15:warning: error return value not checked (defer a.Close()) (errcheck)
-stutter.go:27:6:warning: error return value not checked (doit()           // test for errcheck) (errcheck)
-stutter.go:9::warning: unused global variable unusedGlobal (varcheck)
-stutter.go:13::warning: unused struct field MyStruct.Unused (structcheck)
-stutter.go:12:6:warning: exported type MyStruct should have comment or be unexported (golint)
-stutter.go:16:6:warning: exported type PublicUndocumented should have comment or be unexported (deadcode)
-```
-
 ## Checkstyle XML format
 
 `gometalinter` supports [checkstyle](http://checkstyle.sourceforge.net/)
diff --git a/config.go b/config.go
index 22c0809..fc26d2b 100644
--- a/config.go
+++ b/config.go
@@ -60,7 +60,8 @@ type StringOrLinterConfig LinterConfig
 func (c *StringOrLinterConfig) UnmarshalJSON(raw []byte) error {
 	var linterConfig LinterConfig
 	// first try to un-marshall directly into struct
-	if err := json.Unmarshal(raw, &linterConfig); err == nil {
+	origErr := json.Unmarshal(raw, &linterConfig)
+	if origErr == nil {
 		*c = StringOrLinterConfig(linterConfig)
 		return nil
 	}
@@ -68,7 +69,7 @@ func (c *StringOrLinterConfig) UnmarshalJSON(raw []byte) error {
 	// i.e. bytes didn't represent the struct, treat them as a string
 	var linterSpec string
 	if err := json.Unmarshal(raw, &linterSpec); err != nil {
-		return err
+		return origErr
 	}
 	linter, err := parseLinterConfigSpec("", linterSpec)
 	if err != nil {
diff --git a/linters.go b/linters.go
index 4f31e76..2e96cc6 100644
--- a/linters.go
+++ b/linters.go
@@ -36,7 +36,7 @@ func NewLinter(config LinterConfig) (*Linter, error) {
 		return nil, err
 	}
 	if config.PartitionStrategy == nil {
-		config.PartitionStrategy = partitionToMaxArgSize
+		config.PartitionStrategy = partitionPathsAsDirectories
 	}
 	return &Linter{
 		LinterConfig: config,
@@ -61,6 +61,16 @@ func getLinterByName(name string, overrideConf LinterConfig) *Linter {
 	if val := overrideConf.Pattern; val != "" {
 		conf.Pattern = val
 	}
+	if val := overrideConf.InstallFrom; val != "" {
+		conf.InstallFrom = val
+	}
+	if overrideConf.IsFast {
+		conf.IsFast = true
+	}
+	if val := overrideConf.PartitionStrategy; val != nil {
+		conf.PartitionStrategy = val
+	}
+
 	linter, _ := NewLinter(conf)
 	return linter
 }
@@ -180,7 +190,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           "aligncheck",
 		Pattern:           `^(?:[^:]+: )?(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.+)$`,
 		InstallFrom:       "github.com/opennota/check/cmd/aligncheck",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 		defaultEnabled:    true,
 	},
 	"deadcode": {
@@ -188,7 +198,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           "deadcode",
 		Pattern:           `^deadcode: (?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.*)$`,
 		InstallFrom:       "github.com/tsenart/deadcode",
-		PartitionStrategy: partitionToMaxArgSize,
+		PartitionStrategy: partitionPathsAsDirectories,
 		defaultEnabled:    true,
 	},
 	"dupl": {
@@ -196,7 +206,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `dupl -plumbing -threshold {duplthreshold}`,
 		Pattern:           `^(?P<path>.*?\.go):(?P<line>\d+)-\d+:\s*(?P<message>.*)$`,
 		InstallFrom:       "github.com/mibk/dupl",
-		PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
+		PartitionStrategy: partitionPathsAsFiles,
 		IsFast:            true,
 	},
 	"errcheck": {
@@ -204,7 +214,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `errcheck -abspath`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "github.com/kisielk/errcheck",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 		defaultEnabled:    true,
 	},
 	"gas": {
@@ -212,7 +222,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `gas -fmt=csv`,
 		Pattern:           `^(?P<path>.*?\.go),(?P<line>\d+),(?P<message>[^,]+,[^,]+,[^,]+)`,
 		InstallFrom:       "github.com/GoASTScanner/gas",
-		PartitionStrategy: partitionToMaxArgSize,
+		PartitionStrategy: partitionPathsAsDirectories,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
@@ -221,7 +231,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `goconst -min-occurrences {min_occurrences} -min-length {min_const_length}`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "github.com/jgautheron/goconst/cmd/goconst",
-		PartitionStrategy: partitionToMaxArgSize,
+		PartitionStrategy: partitionPathsAsDirectories,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
@@ -230,7 +240,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `gocyclo -over {mincyclo}`,
 		Pattern:           `^(?P<cyclo>\d+)\s+\S+\s(?P<function>\S+)\s+(?P<path>.*?\.go):(?P<line>\d+):(\d+)$`,
 		InstallFrom:       "github.com/alecthomas/gocyclo",
-		PartitionStrategy: partitionToMaxArgSize,
+		PartitionStrategy: partitionPathsAsDirectories,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
@@ -238,7 +248,7 @@ var defaultLinters = map[string]LinterConfig{
 		Name:              "gofmt",
 		Command:           `gofmt -l -s`,
 		Pattern:           `^(?P<path>.*?\.go)$`,
-		PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
+		PartitionStrategy: partitionPathsAsFiles,
 		IsFast:            true,
 	},
 	"goimports": {
@@ -246,7 +256,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `goimports -l`,
 		Pattern:           `^(?P<path>.*?\.go)$`,
 		InstallFrom:       "golang.org/x/tools/cmd/goimports",
-		PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
+		PartitionStrategy: partitionPathsAsFiles,
 		IsFast:            true,
 	},
 	"golint": {
@@ -254,7 +264,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `golint -min_confidence {min_confidence}`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "github.com/golang/lint/golint",
-		PartitionStrategy: partitionToMaxArgSize,
+		PartitionStrategy: partitionPathsAsDirectories,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
@@ -263,14 +273,14 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `gosimple`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "honnef.co/go/tools/cmd/gosimple",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 	},
 	"gotype": {
 		Name:              "gotype",
 		Command:           `gotype -e {tests=-t}`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "golang.org/x/tools/cmd/gotype",
-		PartitionStrategy: partitionToPackageFileGlobs,
+		PartitionStrategy: partitionPathsAsFilesGroupedByPackage,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
@@ -279,7 +289,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `ineffassign -n`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "github.com/gordonklaus/ineffassign",
-		PartitionStrategy: partitionToMaxArgSize,
+		PartitionStrategy: partitionPathsAsDirectories,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
@@ -288,7 +298,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `interfacer`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "mvdan.cc/interfacer",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 		defaultEnabled:    true,
 	},
 	"lll": {
@@ -296,7 +306,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `lll -g -l {maxlinelength}`,
 		Pattern:           `PATH:LINE:MESSAGE`,
 		InstallFrom:       "github.com/walle/lll/cmd/lll",
-		PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
+		PartitionStrategy: partitionPathsAsFiles,
 		IsFast:            true,
 	},
 	"megacheck": {
@@ -304,7 +314,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `megacheck`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "honnef.co/go/tools/cmd/megacheck",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 		defaultEnabled:    true,
 	},
 	"misspell": {
@@ -312,7 +322,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `misspell -j 1`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "github.com/client9/misspell/cmd/misspell",
-		PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
+		PartitionStrategy: partitionPathsAsFiles,
 		IsFast:            true,
 	},
 	"safesql": {
@@ -320,41 +330,41 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `safesql`,
 		Pattern:           `^- (?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+)$`,
 		InstallFrom:       "github.com/stripe/safesql",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 	},
 	"staticcheck": {
 		Name:              "staticcheck",
 		Command:           `staticcheck`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "honnef.co/go/tools/cmd/staticcheck",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 	},
 	"structcheck": {
 		Name:              "structcheck",
 		Command:           `structcheck {tests=-t}`,
 		Pattern:           `^(?:[^:]+: )?(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.+)$`,
 		InstallFrom:       "github.com/opennota/check/cmd/structcheck",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 		defaultEnabled:    true,
 	},
 	"test": {
 		Name:              "test",
 		Command:           `go test`,
 		Pattern:           `^--- FAIL: .*$\s+(?P<path>.*?\.go):(?P<line>\d+): (?P<message>.*)$`,
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 	},
 	"testify": {
 		Name:              "testify",
 		Command:           `go test`,
 		Pattern:           `Location:\s+(?P<path>.*?\.go):(?P<line>\d+)$\s+Error:\s+(?P<message>[^\n]+)`,
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 	},
 	"unconvert": {
 		Name:              "unconvert",
 		Command:           `unconvert`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "github.com/mdempsky/unconvert",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 		defaultEnabled:    true,
 	},
 	"unparam": {
@@ -362,28 +372,28 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `unparam`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "mvdan.cc/unparam",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 	},
 	"unused": {
 		Name:              "unused",
 		Command:           `unused`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "honnef.co/go/tools/cmd/unused",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 	},
 	"varcheck": {
 		Name:              "varcheck",
 		Command:           `varcheck`,
 		Pattern:           `^(?:[^:]+: )?(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.*)$`,
 		InstallFrom:       "github.com/opennota/check/cmd/varcheck",
-		PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
+		PartitionStrategy: partitionPathsAsPackages,
 		defaultEnabled:    true,
 	},
 	"vet": {
 		Name:              "vet",
 		Command:           `go tool vet`,
 		Pattern:           vetPattern,
-		PartitionStrategy: partitionToPackageFileGlobs,
+		PartitionStrategy: partitionPathsAsFilesGroupedByPackage,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
@@ -391,7 +401,7 @@ var defaultLinters = map[string]LinterConfig{
 		Name:              "vetshadow",
 		Command:           `go tool vet --shadow`,
 		Pattern:           vetPattern,
-		PartitionStrategy: partitionToPackageFileGlobs,
+		PartitionStrategy: partitionPathsAsFilesGroupedByPackage,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
diff --git a/partition.go b/partition.go
index 708beb8..a4a8ce5 100644
--- a/partition.go
+++ b/partition.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"encoding/json"
 	"fmt"
 	"path/filepath"
 )
@@ -10,6 +11,29 @@ const MaxCommandBytes = 32000
 
 type partitionStrategy func([]string, []string) ([][]string, error)
 
+func (ps *partitionStrategy) UnmarshalJSON(raw []byte) error {
+	var strategyName string
+	if err := json.Unmarshal(raw, &strategyName); err != nil {
+		return err
+	}
+
+	switch strategyName {
+	case "directories":
+		*ps = partitionPathsAsDirectories
+	case "files":
+		*ps = partitionPathsAsFiles
+	case "packages":
+		*ps = partitionPathsAsPackages
+	case "files-by-package":
+		*ps = partitionPathsAsFilesGroupedByPackage
+	case "single-directory":
+		*ps = partitionPathsByDirectory
+	default:
+		return fmt.Errorf("unknown parition strategy %s", strategyName)
+	}
+	return nil
+}
+
 func pathsToFileGlobs(paths []string) ([]string, error) {
 	filePaths := []string{}
 	for _, dir := range paths {
@@ -22,7 +46,7 @@ func pathsToFileGlobs(paths []string) ([]string, error) {
 	return filePaths, nil
 }
 
-func partitionToMaxArgSize(cmdArgs []string, paths []string) ([][]string, error) {
+func partitionPathsAsDirectories(cmdArgs []string, paths []string) ([][]string, error) {
 	return partitionToMaxSize(cmdArgs, paths, MaxCommandBytes), nil
 }
 
@@ -72,15 +96,15 @@ func (p *sizePartitioner) end() [][]string {
 	return p.parts
 }
 
-func partitionToMaxArgSizeWithFileGlobs(cmdArgs []string, paths []string) ([][]string, error) {
+func partitionPathsAsFiles(cmdArgs []string, paths []string) ([][]string, error) {
 	filePaths, err := pathsToFileGlobs(paths)
 	if err != nil || len(filePaths) == 0 {
 		return nil, err
 	}
-	return partitionToMaxArgSize(cmdArgs, filePaths)
+	return partitionPathsAsDirectories(cmdArgs, filePaths)
 }
 
-func partitionToPackageFileGlobs(cmdArgs []string, paths []string) ([][]string, error) {
+func partitionPathsAsFilesGroupedByPackage(cmdArgs []string, paths []string) ([][]string, error) {
 	parts := [][]string{}
 	for _, path := range paths {
 		filePaths, err := pathsToFileGlobs([]string{path})
@@ -95,12 +119,12 @@ func partitionToPackageFileGlobs(cmdArgs []string, paths []string) ([][]string,
 	return parts, nil
 }
 
-func partitionToMaxArgSizeWithPackagePaths(cmdArgs []string, paths []string) ([][]string, error) {
+func partitionPathsAsPackages(cmdArgs []string, paths []string) ([][]string, error) {
 	packagePaths, err := pathsToPackagePaths(paths)
 	if err != nil || len(packagePaths) == 0 {
 		return nil, err
 	}
-	return partitionToMaxArgSize(cmdArgs, packagePaths)
+	return partitionPathsAsDirectories(cmdArgs, packagePaths)
 }
 
 func pathsToPackagePaths(paths []string) ([]string, error) {
@@ -129,3 +153,11 @@ func packageNameFromPath(path string) (string, error) {
 	}
 	return "", fmt.Errorf("%s not in GOPATH", path)
 }
+
+func partitionPathsByDirectory(cmdArgs []string, paths []string) ([][]string, error) {
+	parts := [][]string{}
+	for _, path := range paths {
+		parts = append(parts, append(cmdArgs, path))
+	}
+	return parts, nil
+}


Test patch:
diff --git a/config_test.go b/config_test.go
new file mode 100644
index 0000000..f7a4e93
--- /dev/null
+++ b/config_test.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestLinterConfigUnmarshalJSON(t *testing.T) {
+	source := `{
+		"Command": "/bin/custom",
+		"PartitionStrategy": "directories"
+	}`
+	var config StringOrLinterConfig
+	err := json.Unmarshal([]byte(source), &config)
+	require.NoError(t, err)
+	assert.Equal(t, "/bin/custom", config.Command)
+	assert.Equal(t, functionName(partitionPathsAsDirectories), functionName(config.PartitionStrategy))
+}
diff --git a/linters_test.go b/linters_test.go
index 1550a9f..00452f7 100644
--- a/linters_test.go
+++ b/linters_test.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"reflect"
+	"runtime"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -19,10 +21,20 @@ func TestNewLinterWithCustomLinter(t *testing.T) {
 
 func TestGetLinterByName(t *testing.T) {
 	config := LinterConfig{
-		Command: "aligncheck",
-		Pattern: "path",
+		Command:           "aligncheck",
+		Pattern:           "path",
+		InstallFrom:       "./install/path",
+		PartitionStrategy: partitionPathsAsDirectories,
+		IsFast:            true,
 	}
 	overrideConfig := getLinterByName(config.Command, config)
-	require.Equal(t, config.Command, overrideConfig.Command)
-	require.Equal(t, config.Pattern, overrideConfig.Pattern)
+	assert.Equal(t, config.Command, overrideConfig.Command)
+	assert.Equal(t, config.Pattern, overrideConfig.Pattern)
+	assert.Equal(t, config.InstallFrom, overrideConfig.InstallFrom)
+	assert.Equal(t, functionName(config.PartitionStrategy), functionName(overrideConfig.PartitionStrategy))
+	assert.Equal(t, config.IsFast, overrideConfig.IsFast)
+}
+
+func functionName(i interface{}) string {
+	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
 }
diff --git a/partition_test.go b/partition_test.go
index 07ac305..e146486 100644
--- a/partition_test.go
+++ b/partition_test.go
@@ -38,7 +38,7 @@ func TestPartitionToPackageFileGlobs(t *testing.T) {
 		mkGoFile(t, dir, "other.go")
 	}
 
-	parts, err := partitionToPackageFileGlobs(cmdArgs, paths)
+	parts, err := partitionPathsAsFilesGroupedByPackage(cmdArgs, paths)
 	require.NoError(t, err)
 	expected := [][]string{
 		append(cmdArgs, packagePaths(paths[0], "file.go", "other.go")...),
@@ -62,7 +62,7 @@ func TestPartitionToPackageFileGlobsNoFiles(t *testing.T) {
 
 	cmdArgs := []string{"/usr/bin/foo", "-c"}
 	paths := []string{filepath.Join(tmpdir, "one"), filepath.Join(tmpdir, "two")}
-	parts, err := partitionToPackageFileGlobs(cmdArgs, paths)
+	parts, err := partitionPathsAsFilesGroupedByPackage(cmdArgs, paths)
 	require.NoError(t, err)
 	assert.Len(t, parts, 0)
 }
@@ -74,7 +74,7 @@ func TestPartitionToMaxArgSizeWithFileGlobsNoFiles(t *testing.T) {
 
 	cmdArgs := []string{"/usr/bin/foo", "-c"}
 	paths := []string{filepath.Join(tmpdir, "one"), filepath.Join(tmpdir, "two")}
-	parts, err := partitionToMaxArgSizeWithFileGlobs(cmdArgs, paths)
+	parts, err := partitionPathsAsFiles(cmdArgs, paths)
 	require.NoError(t, err)
 	assert.Len(t, parts, 0)
 }
@@ -97,3 +97,18 @@ func fakeGoPath(t *testing.T, path string) func() {
 	require.NoError(t, os.Setenv("GOPATH", path))
 	return func() { require.NoError(t, os.Setenv("GOPATH", oldpath)) }
 }
+
+func TestPartitionPathsByDirectory(t *testing.T) {
+	cmdArgs := []string{"/usr/bin/foo", "-c"}
+	paths := []string{"one", "two", "three"}
+
+	parts, err := partitionPathsByDirectory(cmdArgs, paths)
+	require.NoError(t, err)
+	expected := [][]string{
+		append(cmdArgs, "one"),
+		append(cmdArgs, "two"),
+		append(cmdArgs, "three"),
+	}
+	assert.Equal(t, expected, parts)
+
+}