Repository: alecthomas/gometalinter (go)

Issue:
go type: passing the wrong type of arguments
I'm executing gometalinter via
```
gometalinter ./src
```
And getting the following in the debug output: 
```
executing /Users/charlesrudolph/go/bin/gotype -e ./src/cmd ./src/drivers ./src/exit ./src/git ./src/prompt ./src/script ./src/steps ./src/util
DEBUG: [gotype.9]: warning: /Users/charlesrudolph/go/bin/gotype returned exit status 2: read src/cmd: is a directory
```
It appears when passing multiple arguments to go type it expects each to be a file. To pass directories, I believe we need separate executions. 

Pull request description:
Fix gotype partition strategy
Fixes #348 
Branched from #350 (only the last commit is new)



Patch:
diff --git a/aggregate.go b/aggregate.go
index 18f8f38..f4b44e4 100644
--- a/aggregate.go
+++ b/aggregate.go
@@ -5,27 +5,21 @@ import (
 	"strings"
 )
 
-type (
-	issueKey struct {
-		path      string
-		line, col int
-		message   string
-	}
-
-	multiIssue struct {
-		*Issue
-		linterNames []string
-	}
-)
+type issueKey struct {
+	path      string
+	line, col int
+	message   string
+}
 
-func maybeAggregateIssues(issues chan *Issue) chan *Issue {
-	if !config.Aggregate {
-		return issues
-	}
-	return aggregateIssues(issues)
+type multiIssue struct {
+	*Issue
+	linterNames []string
 }
 
-func aggregateIssues(issues chan *Issue) chan *Issue {
+// AggregateIssueChan reads issues from a channel, aggregates issues which have
+// the same file, line, vol, and message, and returns aggregated issues on
+// a new channel.
+func AggregateIssueChan(issues chan *Issue) chan *Issue {
 	out := make(chan *Issue, 1000000)
 	issueMap := make(map[issueKey]*multiIssue)
 	go func() {
diff --git a/checkstyle.go b/checkstyle.go
index b5f09dc..5122604 100644
--- a/checkstyle.go
+++ b/checkstyle.go
@@ -4,7 +4,7 @@ import (
 	"encoding/xml"
 	"fmt"
 
-	"gopkg.in/alecthomas/kingpin.v3-unstable"
+	kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
 )
 
 type checkstyleOutput struct {
diff --git a/config.go b/config.go
index 9fb7f05..22c0809 100644
--- a/config.go
+++ b/config.go
@@ -51,6 +51,8 @@ type Config struct { // nolint: aligncheck
 	EnableGC        bool
 	Aggregate       bool
 	EnableAll       bool
+
+	formatTemplate *template.Template
 }
 
 type StringOrLinterConfig LinterConfig
@@ -93,14 +95,11 @@ func (td *jsonDuration) Duration() time.Duration {
 	return time.Duration(*td)
 }
 
-// TODO: should be a field on Config struct
-var formatTemplate = &template.Template{}
-
 var sortKeys = []string{"none", "path", "line", "column", "severity", "message", "linter"}
 
 // Configuration defaults.
 var config = &Config{
-	Format: "{{.Path}}:{{.Line}}:{{if .Col}}{{.Col}}{{end}}:{{.Severity}}: {{.Message}} ({{.Linter}})",
+	Format: DefaultIssueFormat,
 
 	Linters: map[string]StringOrLinterConfig{},
 	Severity: map[string]string{
diff --git a/execute.go b/execute.go
index 0885427..e389df2 100644
--- a/execute.go
+++ b/execute.go
@@ -8,14 +8,13 @@ import (
 	"path/filepath"
 	"reflect"
 	"regexp"
-	"sort"
 	"strconv"
 	"strings"
 	"sync"
 	"time"
 
 	"github.com/google/shlex"
-	"gopkg.in/alecthomas/kingpin.v3-unstable"
+	kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
 )
 
 type Vars map[string]string
@@ -41,31 +40,6 @@ func (v Vars) Replace(s string) string {
 	return s
 }
 
-// Severity of linter message.
-type Severity string
-
-// Linter message severity levels.
-const ( // nolint: deadcode
-	Error   Severity = "error"
-	Warning Severity = "warning"
-)
-
-type Issue struct {
-	Linter   string   `json:"linter"`
-	Severity Severity `json:"severity"`
-	Path     string   `json:"path"`
-	Line     int      `json:"line"`
-	Col      int      `json:"col"`
-	Message  string   `json:"message"`
-}
-
-func (i *Issue) String() string {
-	buf := new(bytes.Buffer)
-	err := formatTemplate.Execute(buf, i)
-	kingpin.FatalIfError(err, "Invalid output format")
-	return buf.String()
-}
-
 type linterState struct {
 	*Linter
 	id       int
@@ -243,7 +217,9 @@ func processOutput(dbg debugFunction, state *linterState, out []byte) {
 			group = append(group, fragment)
 		}
 
-		issue := &Issue{Line: 1, Linter: state.Linter.Name}
+		issue, err := NewIssue(state.Linter.Name, config.formatTemplate)
+		kingpin.FatalIfError(err, "Invalid output format")
+
 		for i, name := range re.SubexpNames() {
 			if group[i] == nil {
 				continue
@@ -323,66 +299,16 @@ func resolvePath(path string) string {
 	return path
 }
 
-type sortedIssues struct {
-	issues []*Issue
-	order  []string
-}
-
-func (s *sortedIssues) Len() int      { return len(s.issues) }
-func (s *sortedIssues) Swap(i, j int) { s.issues[i], s.issues[j] = s.issues[j], s.issues[i] }
-
-// nolint: gocyclo
-func (s *sortedIssues) Less(i, j int) bool {
-	l, r := s.issues[i], s.issues[j]
-	for _, key := range s.order {
-		switch key {
-		case "path":
-			if l.Path > r.Path {
-				return false
-			}
-		case "line":
-			if l.Line > r.Line {
-				return false
-			}
-		case "column":
-			if l.Col > r.Col {
-				return false
-			}
-		case "severity":
-			if l.Severity > r.Severity {
-				return false
-			}
-		case "message":
-			if l.Message > r.Message {
-				return false
-			}
-		case "linter":
-			if l.Linter > r.Linter {
-				return false
-			}
-		}
-	}
-	return true
-}
-
 func maybeSortIssues(issues chan *Issue) chan *Issue {
 	if reflect.DeepEqual([]string{"none"}, config.Sort) {
 		return issues
 	}
-	out := make(chan *Issue, 1000000)
-	sorted := &sortedIssues{
-		issues: []*Issue{},
-		order:  config.Sort,
+	return SortIssueChan(issues, config.Sort)
+}
+
+func maybeAggregateIssues(issues chan *Issue) chan *Issue {
+	if !config.Aggregate {
+		return issues
 	}
-	go func() {
-		for issue := range issues {
-			sorted.issues = append(sorted.issues, issue)
-		}
-		sort.Sort(sorted)
-		for _, issue := range sorted.issues {
-			out <- issue
-		}
-		close(out)
-	}()
-	return out
+	return AggregateIssueChan(issues)
 }
diff --git a/issue.go b/issue.go
new file mode 100644
index 0000000..5623c52
--- /dev/null
+++ b/issue.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"sort"
+	"strings"
+	"text/template"
+)
+
+// DefaultIssueFormat used to print an issue
+const DefaultIssueFormat = "{{.Path}}:{{.Line}}:{{if .Col}}{{.Col}}{{end}}:{{.Severity}}: {{.Message}} ({{.Linter}})"
+
+// Severity of linter message
+type Severity string
+
+// Linter message severity levels.
+const ( // nolint: deadcode
+	Error   Severity = "error"
+	Warning Severity = "warning"
+)
+
+type Issue struct {
+	Linter     string   `json:"linter"`
+	Severity   Severity `json:"severity"`
+	Path       string   `json:"path"`
+	Line       int      `json:"line"`
+	Col        int      `json:"col"`
+	Message    string   `json:"message"`
+	formatTmpl *template.Template
+}
+
+// NewIssue returns a new issue. Returns an error if formatTmpl is not a valid
+// template for an Issue.
+func NewIssue(linter string, formatTmpl *template.Template) (*Issue, error) {
+	issue := &Issue{
+		Line:       1,
+		Linter:     linter,
+		formatTmpl: formatTmpl,
+	}
+	err := formatTmpl.Execute(ioutil.Discard, issue)
+	return issue, err
+}
+
+func (i *Issue) String() string {
+	if i.formatTmpl == nil {
+		col := ""
+		if i.Col != 0 {
+			col = fmt.Sprintf("%d", i.Col)
+		}
+		return fmt.Sprintf("%s:%d:%s:%s: %s (%s)", strings.TrimSpace(i.Path), i.Line, col, i.Severity, strings.TrimSpace(i.Message), i.Linter)
+	}
+	buf := new(bytes.Buffer)
+	_ = i.formatTmpl.Execute(buf, i)
+	return buf.String()
+}
+
+type sortedIssues struct {
+	issues []*Issue
+	order  []string
+}
+
+func (s *sortedIssues) Len() int      { return len(s.issues) }
+func (s *sortedIssues) Swap(i, j int) { s.issues[i], s.issues[j] = s.issues[j], s.issues[i] }
+
+func (s *sortedIssues) Less(i, j int) bool {
+	l, r := s.issues[i], s.issues[j]
+	return CompareIssue(*l, *r, s.order)
+}
+
+// CompareIssue two Issues and return true if left should sort before right
+// nolint: gocyclo
+func CompareIssue(l, r Issue, order []string) bool {
+	for _, key := range order {
+		switch {
+		case key == "path" && l.Path != r.Path:
+			return l.Path < r.Path
+		case key == "line" && l.Line != r.Line:
+			return l.Line < r.Line
+		case key == "column" && l.Col != r.Col:
+			return l.Col < r.Col
+		case key == "severity" && l.Severity != r.Severity:
+			return l.Severity < r.Severity
+		case key == "message" && l.Message != r.Message:
+			return l.Message < r.Message
+		case key == "linter" && l.Linter != r.Linter:
+			return l.Linter < r.Linter
+		}
+	}
+	return true
+}
+
+// SortIssueChan reads issues from one channel, sorts them, and returns them to another
+// channel
+func SortIssueChan(issues chan *Issue, order []string) chan *Issue {
+	out := make(chan *Issue, 1000000)
+	sorted := &sortedIssues{
+		issues: []*Issue{},
+		order:  order,
+	}
+	go func() {
+		for issue := range issues {
+			sorted.issues = append(sorted.issues, issue)
+		}
+		sort.Sort(sorted)
+		for _, issue := range sorted.issues {
+			out <- issue
+		}
+		close(out)
+	}()
+	return out
+}
diff --git a/linters.go b/linters.go
index 953934b..4f31e76 100644
--- a/linters.go
+++ b/linters.go
@@ -8,7 +8,7 @@ import (
 	"sort"
 	"strings"
 
-	"gopkg.in/alecthomas/kingpin.v3-unstable"
+	kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
 )
 
 type LinterConfig struct {
@@ -270,7 +270,7 @@ var defaultLinters = map[string]LinterConfig{
 		Command:           `gotype -e {tests=-t}`,
 		Pattern:           `PATH:LINE:COL:MESSAGE`,
 		InstallFrom:       "golang.org/x/tools/cmd/gotype",
-		PartitionStrategy: partitionToMaxArgSize,
+		PartitionStrategy: partitionToPackageFileGlobs,
 		defaultEnabled:    true,
 		IsFast:            true,
 	},
diff --git a/main.go b/main.go
index 6aa60dc..e17e342 100644
--- a/main.go
+++ b/main.go
@@ -14,7 +14,7 @@ import (
 	"text/template"
 	"time"
 
-	"gopkg.in/alecthomas/kingpin.v3-unstable"
+	kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
 )
 
 var (
@@ -222,7 +222,7 @@ Severity override map (default is "warning"):
 func processConfig(config *Config) (include *regexp.Regexp, exclude *regexp.Regexp) {
 	tmpl, err := template.New("output").Parse(config.Format)
 	kingpin.FatalIfError(err, "invalid format %q", config.Format)
-	formatTemplate = tmpl
+	config.formatTemplate = tmpl
 
 	// Linters are by their very nature, short lived, so disable GC.
 	// Reduced (user) linting time on kingpin from 0.97s to 0.64s.


Test patch:
diff --git a/execute_test.go b/execute_test.go
deleted file mode 100644
index 15fb069..0000000
--- a/execute_test.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package main
-
-import (
-	"sort"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestSortedIssues(t *testing.T) {
-	actual := []*Issue{
-		{Path: "b.go", Line: 5},
-		{Path: "a.go", Line: 3},
-		{Path: "b.go", Line: 1},
-		{Path: "a.go", Line: 1},
-	}
-	issues := &sortedIssues{
-		issues: actual,
-		order:  []string{"path", "line"},
-	}
-	sort.Sort(issues)
-	expected := []*Issue{
-		{Path: "a.go", Line: 1},
-		{Path: "a.go", Line: 3},
-		{Path: "b.go", Line: 1},
-		{Path: "b.go", Line: 5},
-	}
-	require.Equal(t, expected, actual)
-}
diff --git a/issue_test.go b/issue_test.go
new file mode 100644
index 0000000..2d456d8
--- /dev/null
+++ b/issue_test.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSortedIssues(t *testing.T) {
+	actual := []*Issue{
+		{Path: "b.go", Line: 5, Col: 1},
+		{Path: "a.go", Line: 3, Col: 2},
+		{Path: "b.go", Line: 1, Col: 3},
+		{Path: "a.go", Line: 1, Col: 4},
+	}
+	issues := &sortedIssues{
+		issues: actual,
+		order:  []string{"path", "line", "column"},
+	}
+	sort.Sort(issues)
+	expected := []*Issue{
+		{Path: "a.go", Line: 1, Col: 4},
+		{Path: "a.go", Line: 3, Col: 2},
+		{Path: "b.go", Line: 1, Col: 3},
+		{Path: "b.go", Line: 5, Col: 1},
+	}
+	require.Equal(t, expected, actual)
+}
+
+func TestCompareOrderWithMessage(t *testing.T) {
+	order := []string{"path", "line", "column", "message"}
+	issueM := Issue{Path: "file.go", Message: "message"}
+	issueU := Issue{Path: "file.go", Message: "unknown"}
+
+	assert.True(t, CompareIssue(issueM, issueU, order))
+	assert.False(t, CompareIssue(issueU, issueM, order))
+}
diff --git a/regressiontests/dupl_test.go b/regressiontests/dupl_test.go
index a9e71d1..f8a9210 100644
--- a/regressiontests/dupl_test.go
+++ b/regressiontests/dupl_test.go
@@ -40,8 +40,8 @@ func two() string {
 `
 
 	expected := Issues{
-		{Linter: "dupl", Severity: "warning", Path: "test.go", Line: 19, Col: 0, Message: "duplicate of test.go:3-17"},
 		{Linter: "dupl", Severity: "warning", Path: "test.go", Line: 3, Col: 0, Message: "duplicate of test.go:19-33"},
+		{Linter: "dupl", Severity: "warning", Path: "test.go", Line: 19, Col: 0, Message: "duplicate of test.go:3-17"},
 	}
 	ExpectIssues(t, "dupl", source, expected)
 }
diff --git a/regressiontests/gosimple_test.go b/regressiontests/gosimple_test.go
index bea7c56..623e873 100644
--- a/regressiontests/gosimple_test.go
+++ b/regressiontests/gosimple_test.go
@@ -22,9 +22,9 @@ func a(ok bool, ch chan bool) {
 }
 `
 	expected := Issues{
+		{Linter: "gosimple", Severity: "warning", Path: "test.go", Line: 4, Col: 2, Message: "should use a simple channel send/receive instead of select with a single case (S1000)"},
 		{Linter: "gosimple", Severity: "warning", Path: "test.go", Line: 8, Col: 2, Message: "should use for range instead of for { select {} } (S1000)"},
 		{Linter: "gosimple", Severity: "warning", Path: "test.go", Line: 14, Col: 5, Message: "should omit comparison to bool constant, can be simplified to ok (S1002)"},
-		{Linter: "gosimple", Severity: "warning", Path: "test.go", Line: 4, Col: 2, Message: "should use a simple channel send/receive instead of select with a single case (S1000)"},
 	}
 	ExpectIssues(t, "gosimple", source, expected)
 }
diff --git a/regressiontests/gotype_test.go b/regressiontests/gotype_test.go
index c18806f..9a66f66 100644
--- a/regressiontests/gotype_test.go
+++ b/regressiontests/gotype_test.go
@@ -1,17 +1,36 @@
 package regressiontests
 
-import "testing"
+import (
+	"testing"
+
+	"fmt"
+
+	"github.com/gotestyourself/gotestyourself/fs"
+	"github.com/stretchr/testify/assert"
+)
 
 func TestGoType(t *testing.T) {
 	t.Parallel()
-	source := `package test
 
-func test() {
-	var foo string
-}
-`
+	dir := fs.NewDir(t, "test-gotype",
+		fs.WithFile("file.go", fileContent("root")),
+		fs.WithDir("sub",
+			fs.WithFile("file.go", fileContent("sub"))))
+	defer dir.Remove()
+
 	expected := Issues{
-		{Linter: "gotype", Severity: "error", Path: "test.go", Line: 4, Col: 6, Message: "foo declared but not used"},
+		{Linter: "gotype", Severity: "error", Path: "file.go", Line: 4, Col: 6, Message: "foo declared but not used"},
+		{Linter: "gotype", Severity: "error", Path: "sub/file.go", Line: 4, Col: 6, Message: "foo declared but not used"},
 	}
-	ExpectIssues(t, "gotype", source, expected)
+	actual := RunLinter(t, "gotype", dir.Path())
+	assert.Equal(t, expected, actual)
+}
+
+func fileContent(pkg string) string {
+	return fmt.Sprintf(`package %s
+
+func badFunction() {
+	var foo string
+}
+	`, pkg)
 }
diff --git a/regressiontests/support.go b/regressiontests/support.go
index 243e57c..2563b8b 100644
--- a/regressiontests/support.go
+++ b/regressiontests/support.go
@@ -8,10 +8,10 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"sort"
 	"strings"
 	"testing"
 
+	"github.com/gotestyourself/gotestyourself/fs"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -35,10 +35,6 @@ func (i *Issue) String() string {
 
 type Issues []Issue
 
-func (e Issues) Len() int           { return len(e) }
-func (e Issues) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
-func (e Issues) Less(i, j int) bool { return e[i].String() < e[j].String() }
-
 // ExpectIssues runs gometalinter and expects it to generate exactly the
 // issues provided.
 func ExpectIssues(t *testing.T, linter string, source string, expected Issues, extraFlags ...string) {
@@ -51,50 +47,60 @@ func ExpectIssues(t *testing.T, linter string, source string, expected Issues, e
 	err = ioutil.WriteFile(testFile, []byte(source), 0644)
 	require.NoError(t, err)
 
-	// Run gometalinter.
+	actual := RunLinter(t, linter, dir, extraFlags...)
+	assert.Equal(t, expected, actual)
+}
+
+// RunLinter runs the gometalinter as a binary against the files at path and
+// returns the issues it encountered
+func RunLinter(t *testing.T, linter string, path string, extraFlags ...string) Issues {
 	binary, cleanup := buildBinary(t)
 	defer cleanup()
-	args := []string{"-d", "--disable-all", "--enable", linter, "--json", dir}
+
+	args := []string{
+		"-d", "--disable-all", "--enable", linter, "--json",
+		"--sort=path", "--sort=line", "--sort=column", "--sort=message",
+		"./...",
+	}
 	args = append(args, extraFlags...)
 	cmd := exec.Command(binary, args...)
+	cmd.Dir = path
+
 	errBuffer := new(bytes.Buffer)
 	cmd.Stderr = errBuffer
-	require.NoError(t, err)
 
 	output, _ := cmd.Output()
+
 	var actual Issues
-	err = json.Unmarshal(output, &actual)
+	err := json.Unmarshal(output, &actual)
 	if !assert.NoError(t, err) {
 		fmt.Printf("Stderr: %s\n", errBuffer)
 		fmt.Printf("Output: %s\n", output)
-		return
-	}
-
-	// Remove output from other linters.
-	actualForLinter := Issues{}
-	for _, issue := range actual {
-		if issue.Linter == linter || linter == "" {
-			// Normalise path.
-			issue.Path = "test.go"
-			issue.Message = strings.Replace(issue.Message, testFile, "test.go", -1)
-			issue.Message = strings.Replace(issue.Message, dir, "", -1)
-			actualForLinter = append(actualForLinter, issue)
-		}
-	}
-	sort.Sort(expected)
-	sort.Sort(actualForLinter)
-
-	if !assert.Equal(t, expected, actualForLinter) {
-		fmt.Printf("Stderr: %s\n", errBuffer)
-		fmt.Printf("Output: %s\n", output)
+		return nil
 	}
+	return filterIssues(actual, linter, path)
 }
 
 func buildBinary(t *testing.T) (string, func()) {
-	tmpdir, err := ioutil.TempDir("", "regression-test")
-	require.NoError(t, err)
-	path := filepath.Join(tmpdir, "binary")
+	tmpdir := fs.NewDir(t, "regression-test-binary")
+	path := tmpdir.Join("gometalinter")
 	cmd := exec.Command("go", "build", "-o", path, "..")
 	require.NoError(t, cmd.Run())
-	return path, func() { os.RemoveAll(tmpdir) }
+	return path, tmpdir.Remove
+}
+
+// filterIssues to just the issues relevant for the current linter and normalize
+// the error message by removing the directory part of the path from both Path
+// and Message
+func filterIssues(issues Issues, linterName string, dir string) Issues {
+	filtered := Issues{}
+	for _, issue := range issues {
+		if issue.Linter == linterName || linterName == "" {
+			issue.Path = strings.Replace(issue.Path, dir+string(os.PathSeparator), "", -1)
+			issue.Message = strings.Replace(issue.Message, dir+string(os.PathSeparator), "", -1)
+			issue.Message = strings.Replace(issue.Message, dir, "", -1)
+			filtered = append(filtered, issue)
+		}
+	}
+	return filtered
 }
diff --git a/regressiontests/unparam_test.go b/regressiontests/unparam_test.go
index 212f8ae..0596d2b 100644
--- a/regressiontests/unparam_test.go
+++ b/regressiontests/unparam_test.go
@@ -1,6 +1,8 @@
 package regressiontests
 
-import "testing"
+import (
+	"testing"
+)
 
 func TestUnparam(t *testing.T) {
 	t.Parallel()
@@ -133,9 +135,6 @@ func AsSliceElem(f FooType) []int {
 
 var SliceElems = []func(FooType) []int{AsSliceElem} `
 	expected := Issues{
-		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 103, Col: 16, Message: "parameter a is unused"},
-		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 103, Col: 27, Message: "parameter r is unused"},
-		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 123, Col: 18, Message: "parameter f is unused"},
 		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 13, Col: 19, Message: "parameter b is unused"},
 		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 20, Col: 20, Message: "parameter f is unused"},
 		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 34, Col: 37, Message: "parameter code is unused"},
@@ -146,6 +145,9 @@ var SliceElems = []func(FooType) []int{AsSliceElem} `
 		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 85, Col: 25, Message: "parameter s is unused"},
 		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 95, Col: 15, Message: "parameter a is unused"},
 		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 95, Col: 26, Message: "parameter b is unused"},
+		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 103, Col: 16, Message: "parameter a is unused"},
+		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 103, Col: 27, Message: "parameter r is unused"},
+		Issue{Linter: "unparam", Severity: "warning", Path: "test.go", Line: 123, Col: 18, Message: "parameter f is unused"},
 	}
 	ExpectIssues(t, "unparam", source, expected)
 }