Repository: mgechev/revive (go)

Issue:
Formatters return an empty string
**Describe the bug**
Five of the revive's formatters always return an empty string for output. Instead of returning the output, they are writing directly to `os.Stdout`. The five formatters are default, friendly, ndjson, plain and unix.

**To Reproduce**
Steps to reproduce the behavior:

Run this test: https://go.dev/play/p/b2yCM6m3PyM

<details>

<summary>expand</summary>

```go

package main

import (
	"go/token"
	"os"
	"strings"
	"testing"

	"github.com/mgechev/revive/formatter"
	"github.com/mgechev/revive/lint"
)

func TestFormatter(t *testing.T) {
	lintFailure := lint.Failure{
		Failure:  "test failure",
		RuleName: "rule",
		Category: "cat",
		Position: lint.FailurePosition{
			Start: token.Position{
				Filename: "test.go",
				Line:     2,
				Column:   5,
			},
			End: token.Position{
				Filename: "test.go",
				Line:     2,
				Column:   10,
			},
		},
	}
	for _, td := range []struct {
		name      string
		formatter lint.Formatter
		want      string
	}{
		{
			name:      "checkstyle",
			formatter: &formatter.Checkstyle{},
			want: `
<?xml version='1.0' encoding='UTF-8'?>
<checkstyle version="5.0">
    <file name="test.go">
      <error line="2" column="5" message="test failure (confidence 0)" severity="warning" source="revive/rule"/>
    </file>
</checkstyle>
`,
		},
		{
			name:      "default",
			formatter: &formatter.Default{},
			want:      `test.go:2:5: test failure`,
		},
		{
			name:      "friendly",
			formatter: &formatter.Friendly{},
			want: `
⚠  https://revive.run/r#rule  test failure  
  test.go:2:5

⚠ 1 problem (0 errors, 1 warning)

Warnings:
  1  rule
`,
		},
		{
			name:      "json",
			formatter: &formatter.JSON{},
			want:      `[{"Severity":"warning","Failure":"test failure","RuleName":"rule","Category":"cat","Position":{"Start":{"Filename":"test.go","Offset":0,"Line":2,"Column":5},"End":{"Filename":"test.go","Offset":0,"Line":2,"Column":10}},"Confidence":0,"ReplacementLine":""}]`,
		},
		{
			name:      "ndjson",
			formatter: &formatter.NDJSON{},
			want:      `{"Severity":"warning","Failure":"test failure","RuleName":"rule","Category":"cat","Position":{"Start":{"Filename":"test.go","Offset":0,"Line":2,"Column":5},"End":{"Filename":"test.go","Offset":0,"Line":2,"Column":10}},"Confidence":0,"ReplacementLine":""}`,
		},
		{
			name:      "plain",
			formatter: &formatter.Plain{},
			want:      `test.go:2:5: test failure https://revive.run/r#rule`,
		},
		{
			name:      "sarif",
			formatter: &formatter.Sarif{},
			want: `
{
  "runs": [
    {
      "results": [
        {
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "test.go"
                },
                "region": {
                  "startColumn": 5,
                  "startLine": 2
                }
              }
            }
          ],
          "message": {
            "text": "test failure"
          },
          "ruleId": "rule"
        }
      ],
      "tool": {
        "driver": {
          "informationUri": "https://revive.run",
          "name": "revive"
        }
      }
    }
  ],
  "version": "2.1.0"
}
`,
		},
		{
			name:      "stylish",
			formatter: &formatter.Stylish{},
			want: `
test.go
  (2, 5)  https://revive.run/r#rule  test failure  


 ✖ 1 problem (0 errors) (1 warnings)
`,
		},
		{
			name:      "unix",
			formatter: &formatter.Unix{},
			want:      `test.go:2:5: [rule] test failure`,
		},
	} {
		t.Run(td.name, func(t *testing.T) {
			dir := t.TempDir()
			realStdout := os.Stdout
			fakeStdout, err := os.Create(dir + "/fakeStdout")
			if err != nil {
				t.Fatal(err)
			}
			os.Stdout = fakeStdout
			defer func() {
				os.Stdout = realStdout
			}()
			failures := make(chan lint.Failure, 10)
			failures <- lintFailure
			close(failures)
			output, err := td.formatter.Format(failures, lint.Config{})
			if err != nil {
				t.Fatal(err)
			}
			os.Stdout = realStdout
			err = fakeStdout.Close()
			if err != nil {
				t.Fatal(err)
			}
			stdout, err := os.ReadFile(fakeStdout.Name())
			if err != nil {
				t.Fatal(err)
			}
			if len(stdout) > 0 {
				t.Errorf("formatter wrote to stdout: %q", stdout)
			}
			got := strings.TrimSpace(output)
			want := strings.TrimSpace(td.want)
			if got != want {
				t.Errorf("got %q, want %q", got, want)
			}
		})
	}
}
```

</details>

**Expected behavior**

I expect the test to pass because the output has been returned from the formatter and nothing was written to stdout.

**Logs**

```
--- FAIL: TestFormatter (0.01s)
    --- FAIL: TestFormatter/default (0.00s)
        testme_test.go:166: formatter wrote to stdout: "test.go:2:5: test failure\n"
        testme_test.go:171: got "", want "test.go:2:5: test failure"
    --- FAIL: TestFormatter/friendly (0.00s)
        testme_test.go:166: formatter wrote to stdout: "  ⚠  https://revive.run/r#rule  test failure  \n  test.go:2:5\n\n⚠ 1 problem (0 errors, 1 warning)\n\nWarnings:\n  1  rule  \n\n"
        testme_test.go:171: got "", want "⚠  https://revive.run/r#rule  test failure  \n  test.go:2:5\n\n⚠ 1 problem (0 errors, 1 warning)\n\nWarnings:\n  1  rule"
    --- FAIL: TestFormatter/ndjson (0.00s)
        testme_test.go:166: formatter wrote to stdout: "{\"Severity\":\"warning\",\"Failure\":\"test failure\",\"RuleName\":\"rule\",\"Category\":\"cat\",\"Position\":{\"Start\":{\"Filename\":\"test.go\",\"Offset\":0,\"Line\":2,\"Column\":5},\"End\":{\"Filename\":\"test.go\",\"Offset\":0,\"Line\":2,\"Column\":10}},\"Confidence\":0,\"ReplacementLine\":\"\"}\n"
        testme_test.go:171: got "", want "{\"Severity\":\"warning\",\"Failure\":\"test failure\",\"RuleName\":\"rule\",\"Category\":\"cat\",\"Position\":{\"Start\":{\"Filename\":\"test.go\",\"Offset\":0,\"Line\":2,\"Column\":5},\"End\":{\"Filename\":\"test.go\",\"Offset\":0,\"Line\":2,\"Column\":10}},\"Confidence\":0,\"ReplacementLine\":\"\"}"
    --- FAIL: TestFormatter/plain (0.00s)
        testme_test.go:166: formatter wrote to stdout: "test.go:2:5: test failure https://revive.run/r#rule\n"
        testme_test.go:171: got "", want "test.go:2:5: test failure https://revive.run/r#rule"
    --- FAIL: TestFormatter/unix (0.00s)
        testme_test.go:166: formatter wrote to stdout: "test.go:2:5: [rule] test failure\n"
        testme_test.go:171: got "", want "test.go:2:5: [rule] test failure"
FAIL
FAIL	github.com/mgechev/revive/testme	0.183s
FAIL
```

**Desktop (please complete the following information):**
 - OS: MacOS 13.5.1
 - Version of Go: 1.21.1

**Additional context**
Add any other context about the problem here.


Pull request description:
formatters return output
This resolves the issue I raised in #920. It also adds a test for formatters. There weren't tests previously, so I added just enough to demonstrate the issue is resolved.

Closes #920


Patch:
diff --git a/README.md b/README.md
index b9f82bd..b628652 100644
--- a/README.md
+++ b/README.md
@@ -125,14 +125,14 @@ Since the default behavior of `revive` is compatible with `golint`, without prov
 `revive` supports a `-config` flag whose value should correspond to a TOML file describing which rules to use for `revive`'s linting. If not provided, `revive` will try to use a global config file (assumed to be located at `$HOME/revive.toml`). Otherwise, if no configuration TOML file is found then `revive` uses a built-in set of default linting rules.
 
 ### Docker
-A volume needs to be mounted to share the current repository with the container.  
+A volume needs to be mounted to share the current repository with the container.
 Please refer to the [bind mounts Docker documentation](https://docs.docker.com/storage/bind-mounts/)
 
 ```bash
 docker run -v "$(pwd)":/var/<repository> ghcr.io/mgechev/revive:v1.1.2-next -config /var/<repository>/revive.toml -formatter stylish ./var/kidle/...
 ```
 
-- `-v` is for the volume  
+- `-v` is for the volume
 - `ghcr.io/mgechev/revive:v1.1.2-next ` is the image name and its version corresponding to `revive` command
 - The provided flags are the same as the binary usage.
 
@@ -628,11 +628,11 @@ With the snippet above we:
 A sample rule implementation can be found [here](/rule/argument-limit.go).
 
 #### Using `revive` as a library
-If a rule is specific to your use case 
+If a rule is specific to your use case
 (i.e. it is not a good candidate to be added to `revive`'s rule set) you can add it to your own linter using `revive` as linting engine.
 
 The following code shows how to use `revive` in your own application.
-In the example only one rule is added (`myRule`), of course, you can add as many as you need to. 
+In the example only one rule is added (`myRule`), of course, you can add as many as you need to.
 Your rules can be configured programmatically or with the standard `revive` configuration file.
 The full rule set of `revive` is also actionable by your application.
 
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index ef0e4b4..f4aeb48 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -33,7 +33,7 @@ List of all available rules.
   - [error-naming](#error-naming)
   - [error-return](#error-return)
   - [error-strings](#error-strings)
-  - [errorf](#errorf)  
+  - [errorf](#errorf)
   - [exported](#exported)
   - [file-header](#file-header)
   - [flag-parameter](#flag-parameter)
@@ -851,8 +851,8 @@ _Description_: This rule warns when [initialism](https://github.com/golang/go/wi
 
 _Configuration_: This rule accepts two slices of strings and one optional slice with single map with named parameters.
 (it's due to TOML hasn't "slice of any" and we keep backward compatibility with previous config version)
-First slice is a whitelist and second one is a blacklist of initialisms. 
-In map, you can add "upperCaseConst=true" parameter to allow `UPPER_CASE` for `const` 
+First slice is a whitelist and second one is a blacklist of initialisms.
+In map, you can add "upperCaseConst=true" parameter to allow `UPPER_CASE` for `const`
 By default, the rule behaves exactly as the alternative in `golint` but optionally, you can relax it (see [golint/lint/issues/89](https://github.com/golang/lint/issues/89))
 
 Example:
diff --git a/formatter/default.go b/formatter/default.go
index f76a7b2..2d5a044 100644
--- a/formatter/default.go
+++ b/formatter/default.go
@@ -1,6 +1,7 @@
 package formatter
 
 import (
+	"bytes"
 	"fmt"
 
 	"github.com/mgechev/revive/lint"
@@ -19,8 +20,9 @@ func (*Default) Name() string {
 
 // Format formats the failures gotten from the lint.
 func (*Default) Format(failures <-chan lint.Failure, _ lint.Config) (string, error) {
+	var buf bytes.Buffer
 	for failure := range failures {
-		fmt.Printf("%v: %s\n", failure.Position.Start, failure.Failure)
+		fmt.Fprintf(&buf, "%v: %s\n", failure.Position.Start, failure.Failure)
 	}
-	return "", nil
+	return buf.String(), nil
 }
diff --git a/formatter/friendly.go b/formatter/friendly.go
index ced8fa4..5ff329a 100644
--- a/formatter/friendly.go
+++ b/formatter/friendly.go
@@ -3,6 +3,7 @@ package formatter
 import (
 	"bytes"
 	"fmt"
+	"io"
 	"sort"
 
 	"github.com/fatih/color"
@@ -31,13 +32,14 @@ func (*Friendly) Name() string {
 
 // Format formats the failures gotten from the lint.
 func (f *Friendly) Format(failures <-chan lint.Failure, config lint.Config) (string, error) {
+	var buf bytes.Buffer
 	errorMap := map[string]int{}
 	warningMap := map[string]int{}
 	totalErrors := 0
 	totalWarnings := 0
 	for failure := range failures {
 		sev := severity(config, failure)
-		f.printFriendlyFailure(failure, sev)
+		f.printFriendlyFailure(&buf, failure, sev)
 		if sev == lint.SeverityWarning {
 			warningMap[failure.RuleName]++
 			totalWarnings++
@@ -47,29 +49,29 @@ func (f *Friendly) Format(failures <-chan lint.Failure, config lint.Config) (str
 			totalErrors++
 		}
 	}
-	f.printSummary(totalErrors, totalWarnings)
-	f.printStatistics(color.RedString("Errors:"), errorMap)
-	f.printStatistics(color.YellowString("Warnings:"), warningMap)
-	return "", nil
+	f.printSummary(&buf, totalErrors, totalWarnings)
+	f.printStatistics(&buf, color.RedString("Errors:"), errorMap)
+	f.printStatistics(&buf, color.YellowString("Warnings:"), warningMap)
+	return buf.String(), nil
 }
 
-func (f *Friendly) printFriendlyFailure(failure lint.Failure, severity lint.Severity) {
-	f.printHeaderRow(failure, severity)
-	f.printFilePosition(failure)
-	fmt.Println()
-	fmt.Println()
+func (f *Friendly) printFriendlyFailure(w io.Writer, failure lint.Failure, severity lint.Severity) {
+	f.printHeaderRow(w, failure, severity)
+	f.printFilePosition(w, failure)
+	fmt.Fprintln(w)
+	fmt.Fprintln(w)
 }
 
-func (f *Friendly) printHeaderRow(failure lint.Failure, severity lint.Severity) {
+func (f *Friendly) printHeaderRow(w io.Writer, failure lint.Failure, severity lint.Severity) {
 	emoji := getWarningEmoji()
 	if severity == lint.SeverityError {
 		emoji = getErrorEmoji()
 	}
-	fmt.Print(f.table([][]string{{emoji, "https://revive.run/r#" + failure.RuleName, color.GreenString(failure.Failure)}}))
+	fmt.Fprint(w, f.table([][]string{{emoji, "https://revive.run/r#" + failure.RuleName, color.GreenString(failure.Failure)}}))
 }
 
-func (*Friendly) printFilePosition(failure lint.Failure) {
-	fmt.Printf("  %s:%d:%d", failure.GetFilename(), failure.Position.Start.Line, failure.Position.Start.Column)
+func (*Friendly) printFilePosition(w io.Writer, failure lint.Failure) {
+	fmt.Fprintf(w, "  %s:%d:%d", failure.GetFilename(), failure.Position.Start.Line, failure.Position.Start.Column)
 }
 
 type statEntry struct {
@@ -77,7 +79,7 @@ type statEntry struct {
 	failures int
 }
 
-func (*Friendly) printSummary(errors, warnings int) {
+func (*Friendly) printSummary(w io.Writer, errors, warnings int) {
 	emoji := getWarningEmoji()
 	if errors > 0 {
 		emoji = getErrorEmoji()
@@ -96,18 +98,18 @@ func (*Friendly) printSummary(errors, warnings int) {
 	}
 	str := fmt.Sprintf("%d %s (%d %s, %d %s)", errors+warnings, problemsLabel, errors, errorsLabel, warnings, warningsLabel)
 	if errors > 0 {
-		fmt.Printf("%s %s\n", emoji, color.RedString(str))
-		fmt.Println()
+		fmt.Fprintf(w, "%s %s\n", emoji, color.RedString(str))
+		fmt.Fprintln(w)
 		return
 	}
 	if warnings > 0 {
-		fmt.Printf("%s %s\n", emoji, color.YellowString(str))
-		fmt.Println()
+		fmt.Fprintf(w, "%s %s\n", emoji, color.YellowString(str))
+		fmt.Fprintln(w)
 		return
 	}
 }
 
-func (f *Friendly) printStatistics(header string, stats map[string]int) {
+func (f *Friendly) printStatistics(w io.Writer, header string, stats map[string]int) {
 	if len(stats) == 0 {
 		return
 	}
@@ -122,8 +124,8 @@ func (f *Friendly) printStatistics(header string, stats map[string]int) {
 	for _, entry := range data {
 		formatted = append(formatted, []string{color.GreenString(fmt.Sprintf("%d", entry.failures)), entry.name})
 	}
-	fmt.Println(header)
-	fmt.Println(f.table(formatted))
+	fmt.Fprintln(w, header)
+	fmt.Fprintln(w, f.table(formatted))
 }
 
 func (*Friendly) table(rows [][]string) string {
diff --git a/formatter/ndjson.go b/formatter/ndjson.go
index a02d9c8..58b35dc 100644
--- a/formatter/ndjson.go
+++ b/formatter/ndjson.go
@@ -1,8 +1,8 @@
 package formatter
 
 import (
+	"bytes"
 	"encoding/json"
-	"os"
 
 	"github.com/mgechev/revive/lint"
 )
@@ -20,7 +20,8 @@ func (*NDJSON) Name() string {
 
 // Format formats the failures gotten from the lint.
 func (*NDJSON) Format(failures <-chan lint.Failure, config lint.Config) (string, error) {
-	enc := json.NewEncoder(os.Stdout)
+	var buf bytes.Buffer
+	enc := json.NewEncoder(&buf)
 	for failure := range failures {
 		obj := jsonObject{}
 		obj.Severity = severity(config, failure)
@@ -30,5 +31,5 @@ func (*NDJSON) Format(failures <-chan lint.Failure, config lint.Config) (string,
 			return "", err
 		}
 	}
-	return "", nil
+	return buf.String(), nil
 }
diff --git a/formatter/plain.go b/formatter/plain.go
index 6e083bc..09ebf6c 100644
--- a/formatter/plain.go
+++ b/formatter/plain.go
@@ -1,6 +1,7 @@
 package formatter
 
 import (
+	"bytes"
 	"fmt"
 
 	"github.com/mgechev/revive/lint"
@@ -19,8 +20,9 @@ func (*Plain) Name() string {
 
 // Format formats the failures gotten from the lint.
 func (*Plain) Format(failures <-chan lint.Failure, _ lint.Config) (string, error) {
+	var buf bytes.Buffer
 	for failure := range failures {
-		fmt.Printf("%v: %s %s\n", failure.Position.Start, failure.Failure, "https://revive.run/r#"+failure.RuleName)
+		fmt.Fprintf(&buf, "%v: %s %s\n", failure.Position.Start, failure.Failure, "https://revive.run/r#"+failure.RuleName)
 	}
-	return "", nil
+	return buf.String(), nil
 }
diff --git a/formatter/unix.go b/formatter/unix.go
index ef2f161..e46f3c2 100644
--- a/formatter/unix.go
+++ b/formatter/unix.go
@@ -1,6 +1,7 @@
 package formatter
 
 import (
+	"bytes"
 	"fmt"
 
 	"github.com/mgechev/revive/lint"
@@ -8,7 +9,8 @@ import (
 
 // Unix is an implementation of the Formatter interface
 // which formats the errors to a simple line based error format
-//  main.go:24:9: [errorf] should replace errors.New(fmt.Sprintf(...)) with fmt.Errorf(...)
+//
+//	main.go:24:9: [errorf] should replace errors.New(fmt.Sprintf(...)) with fmt.Errorf(...)
 type Unix struct {
 	Metadata lint.FormatterMetadata
 }
@@ -20,8 +22,9 @@ func (*Unix) Name() string {
 
 // Format formats the failures gotten from the lint.
 func (*Unix) Format(failures <-chan lint.Failure, _ lint.Config) (string, error) {
+	var buf bytes.Buffer
 	for failure := range failures {
-		fmt.Printf("%v: [%s] %s\n", failure.Position.Start, failure.RuleName, failure.Failure)
+		fmt.Fprintf(&buf, "%v: [%s] %s\n", failure.Position.Start, failure.RuleName, failure.Failure)
 	}
-	return "", nil
+	return buf.String(), nil
 }


Test patch:
diff --git a/config/testdata/enableAllBut2.toml b/config/testdata/enableAllBut2.toml
index adc8f76..0e450ec 100644
--- a/config/testdata/enableAllBut2.toml
+++ b/config/testdata/enableAllBut2.toml
@@ -9,4 +9,4 @@ enableAllRules = true
 [rule.exported]
     disabled=true
 [rule.cyclomatic]
-    disabled=true    
+    disabled=true
diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go
new file mode 100644
index 0000000..e0d4e9d
--- /dev/null
+++ b/formatter/formatter_test.go
@@ -0,0 +1,165 @@
+package formatter_test
+
+import (
+	"go/token"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/mgechev/revive/formatter"
+	"github.com/mgechev/revive/lint"
+)
+
+func TestFormatter(t *testing.T) {
+	lintFailure := lint.Failure{
+		Failure:  "test failure",
+		RuleName: "rule",
+		Category: "cat",
+		Position: lint.FailurePosition{
+			Start: token.Position{
+				Filename: "test.go",
+				Line:     2,
+				Column:   5,
+			},
+			End: token.Position{
+				Filename: "test.go",
+				Line:     2,
+				Column:   10,
+			},
+		},
+	}
+	for _, td := range []struct {
+		formatter lint.Formatter
+		want      string
+	}{
+		{
+			formatter: &formatter.Checkstyle{},
+			want: `
+<?xml version='1.0' encoding='UTF-8'?>
+<checkstyle version="5.0">
+    <file name="test.go">
+      <error line="2" column="5" message="test failure (confidence 0)" severity="warning" source="revive/rule"/>
+    </file>
+</checkstyle>
+`,
+		},
+		{
+			formatter: &formatter.Default{},
+			want:      `test.go:2:5: test failure`,
+		},
+		{
+			formatter: &formatter.Friendly{},
+			want: `
+⚠  https://revive.run/r#rule  test failure  
+  test.go:2:5
+
+⚠ 1 problem (0 errors, 1 warning)
+
+Warnings:
+  1  rule
+`,
+		},
+		{
+			formatter: &formatter.JSON{},
+			want:      `[{"Severity":"warning","Failure":"test failure","RuleName":"rule","Category":"cat","Position":{"Start":{"Filename":"test.go","Offset":0,"Line":2,"Column":5},"End":{"Filename":"test.go","Offset":0,"Line":2,"Column":10}},"Confidence":0,"ReplacementLine":""}]`,
+		},
+		{
+			formatter: &formatter.NDJSON{},
+			want:      `{"Severity":"warning","Failure":"test failure","RuleName":"rule","Category":"cat","Position":{"Start":{"Filename":"test.go","Offset":0,"Line":2,"Column":5},"End":{"Filename":"test.go","Offset":0,"Line":2,"Column":10}},"Confidence":0,"ReplacementLine":""}`,
+		},
+		{
+			formatter: &formatter.Plain{},
+			want:      `test.go:2:5: test failure https://revive.run/r#rule`,
+		},
+		{
+			formatter: &formatter.Sarif{},
+			want: `
+{
+  "runs": [
+    {
+      "results": [
+        {
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "test.go"
+                },
+                "region": {
+                  "startColumn": 5,
+                  "startLine": 2
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "test failure"
+          },
+          "ruleId": "rule"
+        }
+      ],
+      "tool": {
+        "driver": {
+          "informationUri": "https://revive.run",
+          "name": "revive"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}
+`,
+		},
+		{
+			formatter: &formatter.Stylish{},
+			want: `
+test.go
+  (2, 5)  https://revive.run/r#rule  test failure  
+
+
+ ✖ 1 problem (0 errors) (1 warnings)
+`,
+		},
+		{
+			formatter: &formatter.Unix{},
+			want:      `test.go:2:5: [rule] test failure`,
+		},
+	} {
+		t.Run(td.formatter.Name(), func(t *testing.T) {
+			dir := t.TempDir()
+			realStdout := os.Stdout
+			fakeStdout, err := os.Create(dir + "/fakeStdout")
+			if err != nil {
+				t.Fatal(err)
+			}
+			os.Stdout = fakeStdout
+			defer func() {
+				os.Stdout = realStdout
+			}()
+			failures := make(chan lint.Failure, 10)
+			failures <- lintFailure
+			close(failures)
+			output, err := td.formatter.Format(failures, lint.Config{})
+			if err != nil {
+				t.Fatal(err)
+			}
+			os.Stdout = realStdout
+			err = fakeStdout.Close()
+			if err != nil {
+				t.Fatal(err)
+			}
+			stdout, err := os.ReadFile(fakeStdout.Name())
+			if err != nil {
+				t.Fatal(err)
+			}
+			if len(stdout) > 0 {
+				t.Errorf("formatter wrote to stdout: %q", stdout)
+			}
+			got := strings.TrimSpace(output)
+			want := strings.TrimSpace(td.want)
+			if got != want {
+				t.Errorf("got %q, want %q", got, want)
+			}
+		})
+	}
+}
diff --git a/revivelib/core_test.go b/revivelib/core_test.go
index 9dd1505..3e401f6 100644
--- a/revivelib/core_test.go
+++ b/revivelib/core_test.go
@@ -4,6 +4,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/fatih/color"
 	"github.com/mgechev/revive/config"
 	"github.com/mgechev/revive/lint"
 	"github.com/mgechev/revive/revivelib"
@@ -45,6 +46,7 @@ func TestReviveFormat(t *testing.T) {
 	}
 
 	// ACT
+	color.NoColor = true
 	failures, exitCode, err := revive.Format("stylish", failuresChan)
 	// ASSERT
 	if err != nil {
diff --git a/testdata/confusing-results.go b/testdata/confusing-results.go
index 772fc45..fc47abe 100644
--- a/testdata/confusing-results.go
+++ b/testdata/confusing-results.go
@@ -4,7 +4,7 @@ func getfoo() (int, int, error) { // MATCH /unnamed results of the same type may
 
 }
 
-func getBar(a, b int) (int, error, int) { 
+func getBar(a, b int) (int, error, int) {
 }
 
 func Getbaz(a string, b int) (int, float32, string, string) { // MATCH /unnamed results of the same type may be confusing, consider using named results/
diff --git a/testdata/duplicated-imports.go b/testdata/duplicated-imports.go
index 07211c7..f499b21 100644
--- a/testdata/duplicated-imports.go
+++ b/testdata/duplicated-imports.go
@@ -1,7 +1,7 @@
 package fixtures
 
 import(
-	"crypto/md5"  
+	"crypto/md5"
         "strings"
         _ "crypto/md5" // MATCH /Package "crypto/md5" already imported/
         str "strings"  // MATCH /Package "strings" already imported/
diff --git a/testdata/golint/error-strings.go b/testdata/golint/error-strings.go
index 54dab8c..e3ae153 100644
--- a/testdata/golint/error-strings.go
+++ b/testdata/golint/error-strings.go
@@ -16,7 +16,7 @@ func g(x int) error {
 	err = fmt.Errorf("Newlines are really fun\n") // MATCH /error strings should not be capitalized or end with punctuation or a newline/
 	err = errors.New(`too much stuff.`)           // MATCH /error strings should not be capitalized or end with punctuation or a newline/
 	err = errors.New("This %d is too low", x)     // MATCH /error strings should not be capitalized or end with punctuation or a newline/
-		
+
 	// Non-regression test for issue #610
 	d.stack.Push(from)