Repository: mgechev/revive
Pull request #1372 (2025-05-23 18:15:22)

new rule to spot unnecessary use of formatting functions
The rule spots calls to formatting functions without leveraging formatting directives.

Like:
```golang
log.Printf("UDP server exited") // can be replaced with log.Print
fmt.Fprintf(writer, " }") //  fmt.Fprint
message := fmt.Sprintf("contains an invalid SecurityDefinitionsItem") // fmt.Sprint or just "contains an invalid SecurityDefinitionsItem"
```
The implementation doesn't try to be smart to cope with corner cases, it just checks if the _format_ argument of the functions contains `%`. It also avoids relying on type information and limits itself to check some common patterns (like calling `t` the variable holding `*testing.T` or `logger` the variable holding a standard logger instance)

[In the future someone will ask for adding a configuration to define their own functions to check; and after that, as usual, someone else will ask for adding support for regular expressions.] 

I've tested the rule against k8s repo and it found +280 failures


ccoVeille: Isn't there something about this in go vet or go-critic, or maybe perfsprint ?
chavacava: > Isn't there something about this in go vet or go-critic, or maybe perfsprint ?

with 

```go
package fixtures

import (
	"fmt"
	"testing"
)

func unnecessaryFormat(t *testing.T) {
	t.Errorf("expected 4")
	fmt.Printf("nothing")
}
```

neither `go vet` or `perfsprint` detect issues. 
(didn't check with `go-critic`)
ccoVeille: > Isn't there something about this in go vet or go-critic, or maybe perfsprint ?

I checked this morning with my daredevil config

https://github.com/ccoVeille/golangci-lint-config-examples/tree/main/90-daredevil

And nothing was reported about what you tried to detect. So it sounds good 👍

Patch:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index c1269fb..6c3521b 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -61,6 +61,38 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: DavidAnson/markdownlint-cli2-action@v20
+
+      - name: Setup Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: stable
+
+      - name: Setup mdsf
+        uses: hougesen/mdsf@v0.9.5
+
+      - name: Setup goimports
+        # https://pkg.go.dev/golang.org/x/tools/cmd/goimports
+        run: go install golang.org/x/tools/cmd/goimports@v0.33.0
+
+      - name: Setup shfmt
+        # https://github.com/mvdan/sh#shfmt
+        run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.11.0
+
+      - name: Setup taplo
+        # https://taplo.tamasfe.dev/cli/installation/binary.html
+        run: curl -fsSL https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-linux-x86_64.gz | gzip -d - | install -m 755 /dev/stdin /usr/local/bin/taplo
+
+      - name: Verify files format using markdownlint-cli2
+        uses: DavidAnson/markdownlint-cli2-action@v20
         with:
           config: .markdownlint-cli2.yaml
+
+      - name: Verify code snippets using mdsf
+        id: verify_snippets
+        run: mdsf verify --on-missing-language-definition ignore --on-missing-tool-binary fail-fast .
+
+      - name: Show diff when mdsf failed
+        if: failure() && steps.verify_snippets.outcome == 'failure'
+        run: |
+          mdsf format --debug --on-missing-language-definition ignore --on-missing-tool-binary fail-fast .
+          git diff --exit-code
diff --git a/DEVELOPING.md b/DEVELOPING.md
index 360d2da..6f6580e 100644
--- a/DEVELOPING.md
+++ b/DEVELOPING.md
@@ -61,7 +61,7 @@ We can set the banned identifier by using the TOML configuration file:
 
 ```toml
 [rule.ban-struct-name]
-  arguments = ["Foo"]
+arguments = ["Foo"]
 ```
 
 With the snippet above we:
@@ -89,12 +89,17 @@ type Formatter interface {
 
 ### Lint Markdown files
 
-We are using [markdownlint](https://github.com/DavidAnson/markdownlint) for checking Markdown files.
+We use [markdownlint](https://github.com/DavidAnson/markdownlint) and [mdsf](https://github.com/hougesen/mdsf) to check Markdown files.
+`markdownlint` verifies document formatting, such as line length and empty lines, while `mdsf` is responsible for formatting code snippets.
 
 1. Install [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2#install).
-2. Run the following command:
+2. Install [mdsf](https://mdsf.mhouge.dk/#installation) and formatters:
+    - [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) for `go`: `go install golang.org/x/tools/cmd/goimports@latest`
+    - [shfmt](https://github.com/mvdan/sh#shfmt) for `sh, shell, bash`: `go install mvdan.cc/sh/v3/cmd/shfmt@latest`
+    - [taplo](https://taplo.tamasfe.dev/cli/installation/binary.html) for `toml`
+3. Run the following command to check formatting:
 
-```sh
+```shellsession
 $ markdownlint-cli2 .
 Finding: *.{md,markdown} *.md
 Found:
@@ -107,4 +112,14 @@ Linting: 5 file(s)
 Summary: 0 error(s)
 ```
 
-The tool automatically uses the config file [.markdownlint-cli2.yaml](./.markdownlint-cli2.yaml).
+_The `markdownlint-cli2` tool automatically uses the config file [.markdownlint-cli2.yaml](./.markdownlint-cli2.yaml)._
+\
+4. Run the following commands to verify and format code snippets:
+
+```sh
+mdsf verify .
+```
+
+```sh
+mdsf format .
+```
diff --git a/README.md b/README.md
index b55119b..b94bb1a 100644
--- a/README.md
+++ b/README.md
@@ -106,7 +106,7 @@ A volume must 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.3.7 -config /var/<repository>/revive.toml -formatter stylish ./var/kidle/...
+docker run -v "$(pwd)":/var/YOUR_REPOSITORY ghcr.io/mgechev/revive:v1.3.7 -config /var/YOUR_REPOSITORY/revive.toml -formatter stylish ./var/YOUR_REPOSITORY/...
 ```
 
 - `-v` is for the volume
@@ -238,6 +238,7 @@ Using comments, you can disable the linter for the entire file or only a range o
 //revive:disable
 
 func Public() {}
+
 //revive:enable
 ```
 
@@ -251,8 +252,9 @@ You can do the same on a rule level. In case you want to disable only a particul
 ```go
 //revive:disable:unexported-return
 func Public() private {
-  return private
+	return private
 }
+
 //revive:enable:unexported-return
 ```
 
@@ -278,7 +280,7 @@ in the configuration. You can set the severity (defaults to _warning_) of the vi
 
 ```toml
 [directive.specify-disable-reason]
-    severity = "error"
+severity = "error"
 ```
 
 ### Configuration
@@ -305,11 +307,11 @@ warningCode = 0
 # Configuration of the `cyclomatic` rule. Here we specify that
 # the rule should fail if it detects code with higher complexity than 10.
 [rule.cyclomatic]
-  arguments = [10]
+arguments = [10]
 
 # Sets the severity of the `package-comments` rule to "error".
 [rule.package-comments]
-  severity = "error"
+severity = "error"
 ```
 
 By default `revive` will enable only the linting rules that are named in the configuration file.
@@ -328,7 +330,7 @@ For example:
 
 ```toml
 [rule.line-length-limit]
-    Disabled = true
+Disabled = true
 ```
 
 When enabling all rules you still need/can provide specific configurations for rules.
@@ -346,29 +348,29 @@ enableAllRules = true
 
 # Disabled rules
 [rule.blank-imports]
-    Disabled = true
+Disabled = true
 [rule.file-header]
-    Disabled = true
+Disabled = true
 [rule.max-public-structs]
-    Disabled = true
+Disabled = true
 [rule.line-length-limit]
-    Disabled = true
+Disabled = true
 [rule.function-length]
-    Disabled = true
+Disabled = true
 [rule.banned-characters]
-    Disabled = true
+Disabled = true
 
 # Rule tuning
 [rule.argument-limit]
-    Arguments = [5]
+Arguments = [5]
 [rule.cyclomatic]
-    Arguments = [10]
+Arguments = [10]
 [rule.cognitive-complexity]
-    Arguments = [7]
+Arguments = [7]
 [rule.function-result-limit]
-    Arguments = [3]
+Arguments = [3]
 [rule.error-strings]
-    Arguments = ["mypackage.Error"]
+Arguments = ["mypackage.Error"]
 ```
 
 ### Default Configuration
@@ -440,9 +442,9 @@ errorCode = 0
 warningCode = 0
 
 [rule.blank-imports]
-   Exclude=["**/*.pb.go"]
+Exclude = ["**/*.pb.go"]
 [rule.context-as-argument]
-   Exclude=["src/somepkg/*.go", "TEST"]
+Exclude = ["src/somepkg/*.go", "TEST"]
 ```
 
 You can use the following exclude patterns
@@ -538,6 +540,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
 | [`unexported-naming`](./RULES_DESCRIPTIONS.md#unexported-naming)          |  n/a   |  Warns on wrongly named un-exported symbols       |    no    |  no   |
 | [`unexported-return`](./RULES_DESCRIPTIONS.md#unexported-return)   |  n/a   | Warns when a public return is from unexported type.              |   yes    |  yes  |
 | [`unhandled-error`](./RULES_DESCRIPTIONS.md#unhandled-error)   | []string   | Warns on unhandled errors returned by function calls    |    no    |  yes   |
+| [`unnecessary-format`](./RULES_DESCRIPTIONS.md#unnecessary-format)    |  n/a   | Identifies calls to formatting functions where the format string does not contain any formatting verbs          |    no    |  no   |
 | [`unnecessary-stmt`](./RULES_DESCRIPTIONS.md#unnecessary-stmt)    |  n/a   | Suggests removing or simplifying unnecessary statements          |    no    |  no   |
 | [`unreachable-code`](./RULES_DESCRIPTIONS.md#unreachable-code)    |  n/a   | Warns on unreachable code                                        |    no    |  no   |
 | [`unused-parameter`](./RULES_DESCRIPTIONS.md#unused-parameter)    |  n/a   | Suggests to rename or remove unused function parameters          |    no    |  no   |
@@ -560,7 +563,7 @@ in `golint` but optionally, you can relax it (see [golint/lint/issues/89](https:
 
 ```toml
 [rule.var-naming]
-  arguments = [["ID"], ["VM"]]
+arguments = [["ID"], ["VM"]]
 ```
 
 This way, revive will not warn for an identifier called `customId` but will warn that `customVm` should be called `customVM`.
@@ -657,7 +660,9 @@ func (f myRule) Name() string {
 	return "myRule"
 }
 
-func (f myRule) Apply(*lint.File, lint.Arguments) []lint.Failure { ... }
+func (f myRule) Apply(*lint.File, lint.Arguments) []lint.Failure {
+	// ...
+}
 ```
 
 You can still go further and use `revive` without its CLI, as part of your library, or your CLI:
@@ -666,39 +671,39 @@ You can still go further and use `revive` without its CLI, as part of your libra
 package mylib
 
 import (
-	"github.com/mgechev/revive/cli"
-	"github.com/mgechev/revive/revivelib"
+	"github.com/mgechev/revive/config"
 	"github.com/mgechev/revive/lint"
+	"github.com/mgechev/revive/revivelib"
 )
 
 // Error checking removed for clarity
 func LintMyFile(file string) {
-	conf, _:= config.GetConfig("../defaults.toml")
+	conf, _ := config.GetConfig("../defaults.toml")
 
 	revive, _ := revivelib.New(
-		conf,  // Configuration file
-		true,  // Set exit status
-		2048,  // Max open files
+		conf, // Configuration file
+		true, // Set exit status
+		2048, // Max open files
 
 		// Then add as many extra rules as you need
 		revivelib.NewExtraRule(&myRule{}, lint.RuleConfig{}),
 	)
 
 	failuresChan, err := revive.Lint(
- 		revivelib.Include(file),
- 		revivelib.Exclude("./fixtures"),
- 		// You can use as many revivelib.Include or revivelib.Exclude as required
- 	)
-  	if err != nil {
-  	 	panic("Shouldn't have failed: " + err.Error())
-  	}
-
-  	// Now let's return the formatted errors
+		revivelib.Include(file),
+		revivelib.Exclude("./fixtures"),
+		// You can use as many revivelib.Include or revivelib.Exclude as required
+	)
+	if err != nil {
+		panic("Shouldn't have failed: " + err.Error())
+	}
+
+	// Now let's return the formatted errors
 	failures, exitCode, _ := revive.Format("stylish", failuresChan)
 
-  	// failures is the string with all formatted lint error messages
-  	// exit code is 0 if no errors, 1 if errors (unless config options change it)
-  	// ... do something with them
+	// failures is the string with all formatted lint error messages
+	// exit code is 0 if no errors, 1 if errors (unless config options change it)
+	// ... do something with them
 }
 
 type myRule struct{}
@@ -707,7 +712,9 @@ func (f myRule) Name() string {
 	return "myRule"
 }
 
-func (f myRule) Apply(*lint.File, lint.Arguments) []lint.Failure { ... }
+func (f myRule) Apply(*lint.File, lint.Arguments) []lint.Failure {
+	// ...
+}
 ```
 
 ### Custom Formatter
@@ -734,8 +741,8 @@ Here's a basic performance benchmark on MacBook Pro Early 2013 run on Kubernetes
 
 ### golint
 
-```shell
-time golint kubernetes/... > /dev/null
+```shellsession
+$ time golint kubernetes/... > /dev/null
 
 real    0m54.837s
 user    0m57.844s
@@ -744,9 +751,9 @@ sys     0m9.146s
 
 ### revive's speed
 
-```shell
+```shellsession
 # no type checking
-time revive -config untyped.toml kubernetes/... > /dev/null
+$ time revive -config untyped.toml kubernetes/... > /dev/null
 
 real    0m8.471s
 user    0m40.721s
@@ -755,9 +762,9 @@ sys     0m3.262s
 
 Keep in mind that if you use rules that require type checking, the performance may drop to 2x faster than `golint`:
 
-```shell
+```shellsession
 # type checking enabled
-time revive kubernetes/... > /dev/null
+$ time revive kubernetes/... > /dev/null
 
 real    0m26.211s
 user    2m6.708s
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index 183b080..b26d94f 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -78,6 +78,7 @@ List of all available rules.
   - [unexported-naming](#unexported-naming)
   - [unexported-return](#unexported-return)
   - [unhandled-error](#unhandled-error)
+  - [unnecessary-format](#unnecessary-format)
   - [unnecessary-stmt](#unnecessary-stmt)
   - [unreachable-code](#unreachable-code)
   - [unused-parameter](#unused-parameter)
@@ -106,12 +107,16 @@ Examples:
 
 ```toml
 [rule.add-constant]
-  arguments = [{ maxLitCount = "3", allowStrs = "\"\"", allowInts = "0,1,2", allowFloats = "0.0,0.,1.0,1.,2.0,2.", ignoreFuncs = "os\\.*,fmt\\.Println,make" }]
+arguments = [
+  { maxLitCount = "3", allowStrs = "\"\"", allowInts = "0,1,2", allowFloats = "0.0,0.,1.0,1.,2.0,2.", ignoreFuncs = "os\\.*,fmt\\.Println,make" },
+]
 ```
 
 ```toml
 [rule.add-constant]
-  arguments = [{ max-lit-count = "3", allow-strs = "\"\"", allow-ints = "0,1,2", allow-floats = "0.0,0.,1.0,1.,2.0,2.", ignore-funcs = "os\\.*,fmt\\.Println,make" }]
+arguments = [
+  { max-lit-count = "3", allow-strs = "\"\"", allow-ints = "0,1,2", allow-floats = "0.0,0.,1.0,1.,2.0,2.", ignore-funcs = "os\\.*,fmt\\.Println,make" },
+]
 ```
 
 ## argument-limit
@@ -125,7 +130,7 @@ Example:
 
 ```toml
 [rule.argument-limit]
-  arguments = [4]
+arguments = [4]
 ```
 
 ## atomic
@@ -144,7 +149,7 @@ Example:
 
 ```toml
 [rule.banned-characters]
-  arguments = ["Ω","Σ","σ"]
+arguments = ["Ω", "Σ", "σ"]
 ```
 
 ## bare-return
@@ -187,7 +192,7 @@ Example:
 
 ```toml
 [rule.cognitive-complexity]
-  arguments = [7]
+arguments = [7]
 ```
 
 ## comment-spacings
@@ -211,7 +216,7 @@ Example:
 
 ```toml
 [rule.comment-spacings]
-  arguments = ["mypragma:", "+optional"]
+arguments = ["mypragma:", "+optional"]
 ```
 
 ## comments-density
@@ -225,7 +230,7 @@ Example:
 
 ```toml
 [rule.comments-density]
-  arguments = [15]
+arguments = [15]
 ```
 
 ## confusing-naming
@@ -259,12 +264,16 @@ Examples:
 
 ```toml
 [rule.context-as-argument]
-  arguments = [{allowTypesBefore = "*testing.T,*github.com/user/repo/testing.Harness"}]
+arguments = [
+  { allowTypesBefore = "*testing.T,*github.com/user/repo/testing.Harness" },
+]
 ```
 
 ```toml
 [rule.context-as-argument]
-  arguments = [{allow-types-before = "*testing.T,*github.com/user/repo/testing.Harness"}]
+arguments = [
+  { allow-types-before = "*testing.T,*github.com/user/repo/testing.Harness" },
+]
 ```
 
 ## context-keys-type
@@ -284,7 +293,7 @@ Example:
 
 ```toml
 [rule.cyclomatic]
-  arguments = [3]
+arguments = [3]
 ```
 
 ## datarace
@@ -328,12 +337,12 @@ Examples:
 
 ```toml
 [rule.defer]
-  arguments = [["callChain", "loop"]]
+arguments = [["callChain", "loop"]]
 ```
 
 ```toml
 [rule.defer]
-  arguments = [["call-chain", "loop"]]
+arguments = [["call-chain", "loop"]]
 ```
 
 ## dot-imports
@@ -351,12 +360,22 @@ Examples:
 
 ```toml
 [rule.dot-imports]
-  arguments = [{ allowedPackages = ["github.com/onsi/ginkgo/v2","github.com/onsi/gomega"] }]
+arguments = [
+  { allowedPackages = [
+    "github.com/onsi/ginkgo/v2",
+    "github.com/onsi/gomega",
+  ] },
+]
 ```
 
 ```toml
 [rule.dot-imports]
-  arguments = [{ allowed-packages = ["github.com/onsi/ginkgo/v2","github.com/onsi/gomega"] }]
+arguments = [
+  { allowed-packages = [
+    "github.com/onsi/ginkgo/v2",
+    "github.com/onsi/gomega",
+  ] },
+]
 ```
 
 ## duplicated-imports
@@ -370,21 +389,21 @@ _Configuration_: N/A
 _Description_: In Go it is idiomatic to minimize nesting statements, a typical example is to avoid if-then-else constructions.
 This rule spots constructions like
 
-```go
+```golang
 if cond {
-  // do something
+	// do something
 } else {
-  // do other thing
-  return ...
+	// do other thing
+	return ...
 }
 ```
 
 where the `if` condition may be inverted in order to reduce nesting:
 
-```go
+```golang
 if !cond {
-  // do other thing
-  return ...
+	// do other thing
+	return ...
 }
 
 // do something
@@ -400,12 +419,12 @@ Examples:
 
 ```toml
 [rule.early-return]
-  arguments = ["preserveScope", "allowJump"]
+arguments = ["preserveScope", "allowJump"]
 ```
 
 ```toml
 [rule.early-return]
-  arguments = ["preserve-scope", "allow-jump"]
+arguments = ["preserve-scope", "allow-jump"]
 ```
 
 ## empty-block
@@ -436,7 +455,7 @@ Example:
 
 ```toml
 [rule.enforce-map-style]
-  arguments = ["make"]
+arguments = ["make"]
 ```
 
 ## enforce-repeated-arg-type-style
@@ -465,19 +484,19 @@ Example (1):
 
 ```toml
 [rule.enforce-repeated-arg-type-style]
-  arguments = ["short"]
+arguments = ["short"]
 ```
 
 Examples (2):
 
 ```toml
 [rule.enforce-repeated-arg-type-style]
-  arguments = [{ funcArgStyle = "full", funcRetValStyle = "short" }]
+arguments = [{ funcArgStyle = "full", funcRetValStyle = "short" }]
 ```
 
 ```toml
 [rule.enforce-repeated-arg-type-style]
-  arguments = [{ func-arg-style = "full", func-ret-val-style = "short" }]
+arguments = [{ func-arg-style = "full", func-ret-val-style = "short" }]
 ```
 
 ## enforce-slice-style
@@ -497,7 +516,7 @@ Example:
 
 ```toml
 [rule.enforce-slice-style]
-  arguments = ["make"]
+arguments = ["make"]
 ```
 
 ## error-naming
@@ -527,7 +546,7 @@ Example:
 
 ```toml
 [rule.error-strings]
-  arguments = ["xerrors.Errorf"]
+arguments = ["xerrors.Errorf"]
 ```
 
 ## errorf
@@ -563,12 +582,22 @@ Examples:
 
 ```toml
 [rule.exported]
-  arguments = ["checkPrivateReceivers", "disableStutteringCheck", "checkPublicInterface", "disableChecksOnFunctions"]
+arguments = [
+  "checkPrivateReceivers",
+  "disableStutteringCheck",
+  "checkPublicInterface",
+  "disableChecksOnFunctions",
+]
 ```
 
 ```toml
 [rule.exported]
-  arguments = ["check-private-receivers", "disable-stuttering-check", "check-public-interface", "disable-checks-on-functions"]
+arguments = [
+  "check-private-receivers",
+  "disable-stuttering-check",
+  "check-public-interface",
+  "disable-checks-on-functions",
+]
 ```
 
 ## file-header
@@ -581,7 +610,7 @@ Example:
 
 ```toml
 [rule.file-header]
-  arguments = ["This is the text that must appear at the top of source files."]
+arguments = ["This is the text that must appear at the top of source files."]
 ```
 
 ## file-length-limit
@@ -598,12 +627,12 @@ Examples:
 
 ```toml
 [rule.file-length-limit]
-  arguments = [{max=100,skipComments=true,skipBlankLines=true}]
+arguments = [{ max = 100, skipComments = true, skipBlankLines = true }]
 ```
 
 ```toml
 [rule.file-length-limit]
-  arguments = [{max=100,skip-comments=true,skip-blank-lines=true}]
+arguments = [{ max = 100, skip-comments = true, skip-blank-lines = true }]
 ```
 
 ## filename-format
@@ -617,7 +646,7 @@ Example:
 
 ```toml
 [rule.filename-format]
-  arguments=["^[_a-z][_a-z0-9]*\\.go$"]
+arguments = ["^[_a-z][_a-z0-9]*\\.go$"]
 ```
 
 ## flag-parameter
@@ -638,7 +667,7 @@ Example:
 
 ```toml
 [rule.function-length]
-  arguments = [10, 0]
+arguments = [10, 0]
 ```
 
 Will check for functions exceeding 10 statements and will not check the number of lines of functions
@@ -653,7 +682,7 @@ Example:
 
 ```toml
 [rule.function-result-limit]
-  arguments = [3]
+arguments = [3]
 ```
 
 ## get-return
@@ -696,19 +725,19 @@ Example (1):
 
 ```toml
 [rule.import-alias-naming]
-  arguments = ["^[a-z][a-z0-9]{0,}$"]
+arguments = ["^[a-z][a-z0-9]{0,}$"]
 ```
 
 Examples (2):
 
 ```toml
 [rule.import-alias-naming]
-  arguments = [{ allowRegex = "^[a-z][a-z0-9]{0,}$", denyRegex = '^v\d+$' }]
+arguments = [{ allowRegex = "^[a-z][a-z0-9]{0,}$", denyRegex = '^v\d+$' }]
 ```
 
 ```toml
 [rule.import-alias-naming]
-  arguments = [{ allow-regex = "^[a-z][a-z0-9]{0,}$", deny-regex = '^v\d+$' }]
+arguments = [{ allow-regex = "^[a-z][a-z0-9]{0,}$", deny-regex = '^v\d+$' }]
 ```
 
 ## import-shadowing
@@ -729,7 +758,7 @@ Example:
 
 ```toml
 [rule.imports-blocklist]
-  arguments = ["crypto/md5", "crypto/sha1", "crypto/**/pkix"]
+arguments = ["crypto/md5", "crypto/sha1", "crypto/**/pkix"]
 ```
 
 ## increment-decrement
@@ -754,12 +783,12 @@ Examples:
 
 ```toml
 [rule.indent-error-flow]
-  arguments = ["preserveScope"]
+arguments = ["preserveScope"]
 ```
 
 ```toml
 [rule.indent-error-flow]
-  arguments = ["preserve-scope"]
+arguments = ["preserve-scope"]
 ```
 
 ## line-length-limit
@@ -772,7 +801,7 @@ Example:
 
 ```toml
 [rule.line-length-limit]
-  arguments = [80]
+arguments = [80]
 ```
 
 ## max-control-nesting
@@ -785,7 +814,7 @@ Example:
 
 ```toml
 [rule.max-control-nesting]
-  arguments = [3]
+arguments = [3]
 ```
 
 ## max-public-structs
@@ -801,7 +830,7 @@ Example:
 
 ```toml
 [rule.max-public-structs]
-  arguments = [3]
+arguments = [3]
 ```
 
 ## modifies-parameter
@@ -838,13 +867,13 @@ _Configuration_: N/A
 
 Example:
 
-```go
+```golang
 if isGenerated(content) && !config.IgnoreGeneratedHeader {
 ```
 
 Swap left and right side :
 
-```go
+```golang
 if !config.IgnoreGeneratedHeader && isGenerated(content) {
 ```
 
@@ -895,12 +924,12 @@ Examples:
 
 ```toml
 [rule.receiver-naming]
-  arguments = [{maxLength=2}]
+arguments = [{ maxLength = 2 }]
 ```
 
 ```toml
 [rule.receiver-naming]
-  arguments = [{max-length=2}]
+arguments = [{ max-length = 2 }]
 ```
 
 ## redefines-builtin-id
@@ -962,12 +991,28 @@ Example:
 
 ```toml
 [rule.string-format]
-  arguments = [
-    ["core.WriteError[1].Message", "/^([^A-Z]|$)/", "must not start with a capital letter"],
-    ["fmt.Errorf[0]", "/(^|[^\\.!?])$/", "must not end in punctuation"],
-    ["panic", "/^[^\\n]*$/", "must not contain line breaks"],
-    ["fmt.Errorf[0],core.WriteError[1].Message", "!/^.*%w.*$/", "must not contain '%w'"],
-  ]
+arguments = [
+  [
+    "core.WriteError[1].Message",
+    "/^([^A-Z]|$)/",
+    "must not start with a capital letter",
+  ],
+  [
+    "fmt.Errorf[0]",
+    "/(^|[^\\.!?])$/",
+    "must not end in punctuation",
+  ],
+  [
+    "panic",
+    "/^[^\\n]*$/",
+    "must not contain line breaks",
+  ],
+  [
+    "fmt.Errorf[0],core.WriteError[1].Message",
+    "!/^.*%w.*$/",
+    "must not contain '%w'",
+  ],
+]
 ```
 
 ## string-of-int
@@ -990,7 +1035,7 @@ To accept the `inline` option in JSON tags (and `outline` and `gnu` in BSON tags
 
 ```toml
 [rule.struct-tag]
-  arguments = ["json,inline", "bson,outline,gnu"]
+arguments = ["json,inline", "bson,outline,gnu"]
 ```
 
 ## superfluous-else
@@ -1006,12 +1051,12 @@ Examples:
 
 ```toml
 [rule.superfluous-else]
-  arguments = ["preserveScope"]
+arguments = ["preserveScope"]
 ```
 
 ```toml
 [rule.superfluous-else]
-  arguments = ["preserve-scope"]
+arguments = ["preserve-scope"]
 ```
 
 ## time-date
@@ -1025,11 +1070,13 @@ _Example_:
 Here the leading zeros are defining integers with octal notation
 
 ```go
+import "time"
+
 var (
-  // here we can imagine zeroes were used for padding purpose
-  a = time.Date(2023, 1, 2, 3, 4, 0, 00000000, time.UTC) // 00000000 is octal and equals 0 in decimal
-  b = time.Date(2023, 1, 2, 3, 4, 0, 00000006, time.UTC) // 00000006 is octal and equals 6 in decimal
-  c = time.Date(2023, 1, 2, 3, 4, 0, 00123456, time.UTC) // 00123456 is octal and equals 42798 in decimal
+	// here we can imagine zeroes were used for padding purpose
+	a = time.Date(2023, 1, 2, 3, 4, 0, 00000000, time.UTC) // 00000000 is octal and equals 0 in decimal
+	b = time.Date(2023, 1, 2, 3, 4, 0, 00000006, time.UTC) // 00000006 is octal and equals 6 in decimal
+	c = time.Date(2023, 1, 2, 3, 4, 0, 00123456, time.UTC) // 00123456 is octal and equals 42798 in decimal
 )
 ```
 
@@ -1044,15 +1091,17 @@ This rule also reports strange notations used with time.Date.
 Example:
 
 ```go
+import "time"
+
 var _ = time.Date(
-		0x7e7,    // hexadecimal notation: use 2023 instead of 0x7e7/
-		0b1,      // binary notation: use 1 instead of 0b1/
-		0x_2,     // hexadecimal notation: use 2 instead of 0x_2/
-		1_3,      // alternative notation: use 13 instead of 1_3/
-		1e1,      // exponential notation: use 10 instead of 1e1/
-		0.,       // float literal: use 0 instead of 0./
-		0x1.Fp+6, // float literal: use 124 instead of 0x1.Fp+6/
-		time.UTC)
+	0x7e7,    // hexadecimal notation: use 2023 instead of 0x7e7/
+	0b1,      // binary notation: use 1 instead of 0b1/
+	0x_2,     // hexadecimal notation: use 2 instead of 0x_2/
+	1_3,      // alternative notation: use 13 instead of 1_3/
+	1e1,      // exponential notation: use 10 instead of 1e1/
+	0.,       // float literal: use 0 instead of 0./
+	0x1.Fp+6, // float literal: use 124 instead of 0x1.Fp+6/
+	time.UTC)
 ```
 
 All these are considered to be an uncommon usage of time.Date, are reported with a 0.8 confidence.
@@ -1060,6 +1109,8 @@ All these are considered to be an uncommon usage of time.Date, are reported with
 Note: even if 00, 01, 02, 03, 04, 05, 06, 07 are octal numbers, they can be considered as valid, and reported with 0.5 confidence.
 
 ```go
+import "time"
+
 var _ = time.Date(2023, 01, 02, 03, 04, 00, 0, time.UTC)
 ```
 
@@ -1086,7 +1137,7 @@ _Configuration_: list of key-value-pair-map (`[]map[string]any`).
 - `acceptIgnoredAssertionResult` (`acceptignoredassertionresult`, `accept-ignored-assertion-result`): (bool) default `false`,
 set it to `true` to accept ignored type assertion results like this:
 
-```go
+```golang
 foo, _ := bar(.*Baz).
 //   ^
 ```
@@ -1095,12 +1146,12 @@ Examples:
 
 ```toml
 [rule.unchecked-type-assertion]
-  arguments = [{acceptIgnoredAssertionResult=true}]
+arguments = [{ acceptIgnoredAssertionResult = true }]
 ```
 
 ```toml
 [rule.unchecked-type-assertion]
-  arguments = [{accept-ignored-assertion-result=true}]
+arguments = [{ accept-ignored-assertion-result = true }]
 ```
 
 ## unconditional-recursion
@@ -1132,9 +1183,22 @@ Example:
 
 ```toml
 [rule.unhandled-error]
-  arguments = ["os\.(Create|WriteFile|Chmod)", "fmt\.Print", "myFunction", "net\..*", "bytes\.Buffer\.Write"]
+arguments = [
+  'os\.(Create|WriteFile|Chmod)',
+  'fmt\.Print',
+  'myFunction',
+  'net\..*',
+  'bytes\.Buffer\.Write',
+]
 ```
 
+## unnecessary-format
+
+_Description_: This rule identifies calls to formatting functions where the format string does not contain any formatting verbs
+and recommends switching to the non-formatting, more efficient alternative.
+
+_Configuration_: N/A
+
 ## unnecessary-stmt
 
 _Description_: This rule suggests to remove redundant statements like a `break` at the end of a case block, for improving the code's readability.
@@ -1164,12 +1228,12 @@ func SomeFunc(_someObj *MyStruct) {} // matches rule
 
 ```toml
 [rule.unused-parameter]
-  arguments = [{ allowRegex = "^_" }]
+arguments = [{ allowRegex = "^_" }]
 ```
 
 ```toml
 [rule.unused-parameter]
-  arguments = [{ allow-regex = "^_" }]
+arguments = [{ allow-regex = "^_" }]
 ```
 
 ## unused-receiver
@@ -1189,12 +1253,12 @@ func (_my *MyStruct) SomeMethod() {} // matches rule
 
 ```toml
 [rule.unused-receiver]
-  arguments = [{ allowRegex = "^_" }]
+arguments = [{ allowRegex = "^_" }]
 ```
 
 ```toml
 [rule.unused-receiver]
-  arguments = [{ allow-regex = "^_" }]
+arguments = [{ allow-regex = "^_" }]
 ```
 
 ## use-any
@@ -1248,32 +1312,32 @@ Examples:
 
 ```toml
 [rule.var-naming]
-  arguments = [["ID"], ["VM"], [{upperCaseConst=true}]]
+arguments = [["ID"], ["VM"], [{ upperCaseConst = true }]]
 ```
 
 ```toml
 [rule.var-naming]
-  arguments = [[], [], [{skipPackageNameChecks=true}]]
+arguments = [[], [], [{ skipPackageNameChecks = true }]]
 ```
 
 ```toml
 [rule.var-naming]
-  arguments = [[], [], [{extraBadPackageNames=["helpers", "models"]}]]
+arguments = [[], [], [{ extraBadPackageNames = ["helpers", "models"] }]]
 ```
 
 ```toml
 [rule.var-naming]
-  arguments = [["ID"], ["VM"], [{upper-case-const=true}]]
+arguments = [["ID"], ["VM"], [{ upper-case-const = true }]]
 ```
 
 ```toml
 [rule.var-naming]
-  arguments = [[], [], [{skip-package-name-checks=true}]]
+arguments = [[], [], [{ skip-package-name-checks = true }]]
 ```
 
 ```toml
 [rule.var-naming]
-  arguments = [[], [], [{extra-bad-package-names=["helpers", "models"]}]]
+arguments = [[], [], [{ extra-bad-package-names = ["helpers", "models"] }]]
 ```
 
 ## waitgroup-by-value
diff --git a/cli/main.go b/cli/main.go
index 07e0024..c703643 100644
--- a/cli/main.go
+++ b/cli/main.go
@@ -101,24 +101,24 @@ var (
 
 var originalUsage = flag.Usage
 
-func getLogo() string {
+func logo() string {
 	return color.YellowString(` _ __ _____   _(_)__  _____
 | '__/ _ \ \ / / \ \ / / _ \
 | | |  __/\ V /| |\ V /  __/
 |_|  \___| \_/ |_| \_/ \___|`)
 }
 
-func getCall() string {
+func call() string {
 	return color.MagentaString("revive -config c.toml -formatter friendly -exclude a.go -exclude b.go ./...")
 }
 
-func getBanner() string {
+func banner() string {
 	return fmt.Sprintf(`
 %s
 
 Example:
   %s
-`, getLogo(), getCall())
+`, logo(), call())
 }
 
 func buildDefaultConfigPath() string {
@@ -150,7 +150,7 @@ func initConfig() {
 	}
 
 	flag.Usage = func() {
-		fmt.Println(getBanner())
+		fmt.Println(banner())
 		originalUsage()
 	}
 
diff --git a/config/config.go b/config/config.go
index 0f9ee9e..d3e47c3 100644
--- a/config/config.go
+++ b/config/config.go
@@ -102,6 +102,7 @@ var allRules = append([]lint.Rule{
 	&rule.RedundantBuildTagRule{},
 	&rule.UseErrorsNewRule{},
 	&rule.RedundantTestMainExitRule{},
+	&rule.UnnecessaryFormatRule{},
 }, defaultRules...)
 
 // allFormatters is a list of all available formatters to output the linting results.
diff --git a/formatter/checkstyle.go b/formatter/checkstyle.go
index 8fe85fa..9b91d8e 100644
--- a/formatter/checkstyle.go
+++ b/formatter/checkstyle.go
@@ -43,7 +43,7 @@ func (*Checkstyle) Format(failures <-chan lint.Failure, config lint.Config) (str
 			Severity:   severity(config, failure),
 			RuleName:   failure.RuleName,
 		}
-		fn := failure.GetFilename()
+		fn := failure.Filename()
 		if issues[fn] == nil {
 			issues[fn] = []issue{}
 		}
diff --git a/formatter/friendly.go b/formatter/friendly.go
index 570589d..6a16d6f 100644
--- a/formatter/friendly.go
+++ b/formatter/friendly.go
@@ -68,7 +68,7 @@ func (*Friendly) printHeaderRow(sb *strings.Builder, failure lint.Failure, sever
 }
 
 func (*Friendly) printFilePosition(sb *strings.Builder, failure lint.Failure) {
-	fmt.Fprintf(sb, "  %s:%d:%d", failure.GetFilename(), failure.Position.Start.Line, failure.Position.Start.Column)
+	fmt.Fprintf(sb, "  %s:%d:%d", failure.Filename(), failure.Position.Start.Line, failure.Position.Start.Column)
 }
 
 type statEntry struct {
diff --git a/formatter/stylish.go b/formatter/stylish.go
index b812309..f44ada8 100644
--- a/formatter/stylish.go
+++ b/formatter/stylish.go
@@ -27,7 +27,7 @@ func formatFailure(failure lint.Failure, severity lint.Severity) []string {
 	if severity == lint.SeverityWarning {
 		fName = color.YellowString(fURL)
 	}
-	return []string{failure.GetFilename(), pos, fName, fString}
+	return []string{failure.Filename(), pos, fName, fString}
 }
 
 // Format formats the failures gotten from the lint.
diff --git a/internal/astutils/ast_utils.go b/internal/astutils/ast_utils.go
index cc79887..b515a3b 100644
--- a/internal/astutils/ast_utils.go
+++ b/internal/astutils/ast_utils.go
@@ -3,6 +3,7 @@ package astutils
 
 import (
 	"go/ast"
+	"go/token"
 	"slices"
 )
 
@@ -76,3 +77,10 @@ func getFieldTypeName(typ ast.Expr) string {
 		return "UNHANDLED_TYPE"
 	}
 }
+
+// IsStringLiteral returns true if the given expression is a string literal, false otherwise
+func IsStringLiteral(e ast.Expr) bool {
+	sl, ok := e.(*ast.BasicLit)
+
+	return ok && sl.Kind == token.STRING
+}
diff --git a/lint/failure.go b/lint/failure.go
index 48095f9..c9a6d72 100644
--- a/lint/failure.go
+++ b/lint/failure.go
@@ -81,7 +81,14 @@ type Failure struct {
 }
 
 // GetFilename returns the filename.
+//
+// Deprecated: Use [Filename].
 func (f *Failure) GetFilename() string {
+	return f.Filename()
+}
+
+// Filename returns the filename.
+func (f *Failure) Filename() string {
 	return f.Position.Start.Filename
 }
 
diff --git a/mdsf.json b/mdsf.json
new file mode 100644
index 0000000..4a1c819
--- /dev/null
+++ b/mdsf.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "https://raw.githubusercontent.com/hougesen/mdsf/HEAD/schemas/v0.9.5/mdsf.schema.json",
+  "format_finished_document": false,
+  "languages": {
+    "go": "goimports",
+    "shell": "shfmt",
+    "toml": "taplo"
+  },
+  "language_aliases": {
+    "bash": "shell",
+    "sh": "shell"
+  }
+}
diff --git a/revivelib/core.go b/revivelib/core.go
index 86aff1a..0e237f4 100755
--- a/revivelib/core.go
+++ b/revivelib/core.go
@@ -75,9 +75,9 @@ func (r *Revive) Lint(patterns ...*LintPattern) (<-chan lint.Failure, error) {
 
 	for _, lintpkg := range patterns {
 		if lintpkg.IsExclude() {
-			excludePatterns = append(excludePatterns, lintpkg.GetPattern())
+			excludePatterns = append(excludePatterns, lintpkg.Pattern())
 		} else {
-			includePatterns = append(includePatterns, lintpkg.GetPattern())
+			includePatterns = append(includePatterns, lintpkg.Pattern())
 		}
 	}
 
diff --git a/revivelib/pattern.go b/revivelib/pattern.go
index fe87fc0..16d8d4d 100644
--- a/revivelib/pattern.go
+++ b/revivelib/pattern.go
@@ -12,7 +12,14 @@ func (p *LintPattern) IsExclude() bool {
 }
 
 // GetPattern - returns the actual pattern
+//
+// Deprecated: Use [Pattern].
 func (p *LintPattern) GetPattern() string {
+	return p.Pattern()
+}
+
+// Pattern - returns the actual pattern
+func (p *LintPattern) Pattern() string {
 	return p.pattern
 }
 
diff --git a/rule/error_naming.go b/rule/error_naming.go
index 5ae4908..e4f0250 100644
--- a/rule/error_naming.go
+++ b/rule/error_naming.go
@@ -61,6 +61,17 @@ func (w lintErrors) Visit(_ ast.Node) ast.Visitor {
 			}
 
 			id := spec.Names[0]
+			if id.Name == "_" {
+				// avoid false positive for blank identifier
+
+				// The fact that the error variable is not used
+				// is out of the scope of the rule
+
+				// This pattern that can be found in benchmarks and examples
+				// should be allowed.
+				continue
+			}
+
 			prefix := "err"
 			if id.IsExported() {
 				prefix = "Err"
diff --git a/rule/unnecessary_format.go b/rule/unnecessary_format.go
new file mode 100644
index 0000000..004cd40
--- /dev/null
+++ b/rule/unnecessary_format.go
@@ -0,0 +1,129 @@
+package rule
+
+import (
+	"fmt"
+	"go/ast"
+	"strings"
+
+	"github.com/mgechev/revive/internal/astutils"
+	"github.com/mgechev/revive/lint"
+)
+
+// UnnecessaryFormatRule spots calls to formatting functions without leveraging formatting directives.
+type UnnecessaryFormatRule struct{}
+
+// Apply applies the rule to given file.
+func (*UnnecessaryFormatRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+	var failures []lint.Failure
+
+	fileAst := file.AST
+	walker := lintUnnecessaryFormat{
+		onFailure: func(failure lint.Failure) {
+			failures = append(failures, failure)
+		},
+	}
+
+	ast.Walk(walker, fileAst)
+
+	return failures
+}
+
+// Name returns the rule name.
+func (*UnnecessaryFormatRule) Name() string {
+	return "unnecessary-format"
+}
+
+type lintUnnecessaryFormat struct {
+	onFailure func(lint.Failure)
+}
+
+type formattingSpec struct {
+	formatArgPosition byte
+	alternative       string
+}
+
+var formattingFuncs = map[string]formattingSpec{
+	"fmt.Appendf": {1, `"fmt.Append"`},
+	"fmt.Errorf":  {0, `"errors.New"`},
+	"fmt.Fprintf": {1, `"fmt.Fprint"`},
+	"fmt.Fscanf":  {1, `"fmt.Fscan" or "fmt.Fscanln"`},
+	"fmt.Printf":  {0, `"fmt.Print" or "fmt.Println"`},
+	"fmt.Scanf":   {0, `"fmt.Scan"`},
+	"fmt.Sprintf": {0, `"fmt.Sprint" or just the string itself"`},
+	"fmt.Sscanf":  {1, `"fmt.Sscan"`},
+	// standard logging functions
+	"log.Fatalf": {0, `"log.Fatal"`},
+	"log.Panicf": {0, `"log.Panic"`},
+	"log.Printf": {0, `"log.Print"`},
+	// Variable logger possibly being the std logger
+	// Will trigger a false positive if all the following holds:
+	//   1. the variable is not the std logger
+	//   2. the Fatalf/Panicf/Printf method expects a string as first argument
+	//      but the string is not expected to be a format string
+	//   3. the actual first argument is a string literal
+	//   4. the actual first argument does not contain a %
+	"logger.Fatalf": {0, `"logger.Fatal"`},
+	"logger.Panicf": {0, `"logger.Panic"`},
+	"logger.Printf": {0, `"logger.Print"`},
+	// standard testing functions
+	// Variable t possibly being a testing.T
+	// False positive risk: see comment on logger
+	"t.Errorf": {0, `"t.Error"`},
+	"t.Fatalf": {0, `"t.Fatal"`},
+	"t.Logf":   {0, `"t.Log"`},
+	"t.Skipf":  {0, `"t.Skip"`},
+	// Variable b possibly being a testing.B
+	// False positive risk: see comment on logger
+	"b.Errorf": {0, `"b.Error"`},
+	"b.Fatalf": {0, `"b.Fatal"`},
+	"b.Logf":   {0, `"b.Log"`},
+	"b.Skipf":  {0, `"b.Skip"`},
+	// Variable f possibly being a testing.F
+	// False positive risk: see comment on logger
+	"f.Errorf": {0, `"f.Error"`},
+	"f.Fatalf": {0, `"f.Fatal"`},
+	"f.Logf":   {0, `"f.Log"`},
+	"f.Skipf":  {0, `"f.Skip"`},
+	// standard trace functions
+	"trace.Logf": {2, `"trace.Log"`},
+}
+
+func (w lintUnnecessaryFormat) Visit(n ast.Node) ast.Visitor {
+	ce, ok := n.(*ast.CallExpr)
+	if !ok || len(ce.Args) < 1 {
+		return w
+	}
+
+	funcName := gofmt(ce.Fun)
+	spec, ok := formattingFuncs[funcName]
+	if !ok {
+		return w
+	}
+
+	pos := int(spec.formatArgPosition)
+	if len(ce.Args) <= pos {
+		return w // not enough params /!\
+	}
+
+	arg := ce.Args[pos]
+	if !astutils.IsStringLiteral(arg) {
+		return w
+	}
+
+	format := gofmt(arg)
+
+	if strings.Contains(format, `%`) {
+		return w
+	}
+
+	failure := lint.Failure{
+		Category:   lint.FailureCategoryOptimization,
+		Node:       ce.Fun,
+		Confidence: 0.8,
+		Failure:    fmt.Sprintf("unnecessary use of formatting function %q, you can replace it with %s", funcName, spec.alternative),
+	}
+
+	w.onFailure(failure)
+
+	return w
+}


Test patch:
diff --git a/test/golint_test.go b/test/golint_test.go
index 263b089..70dd577 100644
--- a/test/golint_test.go
+++ b/test/golint_test.go
@@ -63,12 +63,9 @@ func TestAll(t *testing.T) {
 				t.Fatalf("Failed reading %s: %v", fi.Name(), err)
 			}
 
-			fileInfo, err := os.Stat(filePath)
-			if err != nil {
-				t.Fatalf("Failed reading %s: %v", fi.Name(), err)
-			}
+			ins := parseInstructions(t, filePath, src)
 
-			if err := assertFailures(t, filepath.Dir(baseDir), fileInfo, src, rules, map[string]lint.RuleConfig{}); err != nil {
+			if err := assertFailures(t, filePath, rules, map[string]lint.RuleConfig{}, ins); err != nil {
 				t.Errorf("Linting %s: %v", fi.Name(), err)
 			}
 		})
diff --git a/test/unnecessary_format_test.go b/test/unnecessary_format_test.go
new file mode 100644
index 0000000..9269e09
--- /dev/null
+++ b/test/unnecessary_format_test.go
@@ -0,0 +1,12 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/mgechev/revive/lint"
+	"github.com/mgechev/revive/rule"
+)
+
+func TestUnnecessaryFormat(t *testing.T) {
+	testRule(t, "unnecessary_format", &rule.UnnecessaryFormatRule{}, &lint.RuleConfig{})
+}
diff --git a/test/utils_test.go b/test/utils_test.go
index 6ffc509..63b5b2c 100644
--- a/test/utils_test.go
+++ b/test/utils_test.go
@@ -42,10 +42,7 @@ func testRule(t *testing.T, filename string, rule lint.Rule, config ...*lint.Rul
 	if err != nil {
 		t.Fatalf("Bad filename path in test for %s: %v", rule.Name(), err)
 	}
-	stat, err := os.Stat(fullFilePath)
-	if err != nil {
-		t.Fatalf("Cannot get file info for %s: %v", rule.Name(), err)
-	}
+
 	var ruleConfig lint.RuleConfig
 	c := map[string]lint.RuleConfig{}
 	if len(config) > 0 {
@@ -54,19 +51,19 @@ func testRule(t *testing.T, filename string, rule lint.Rule, config ...*lint.Rul
 	}
 	configureRule(t, rule, ruleConfig.Arguments)
 
-	if parseInstructions(t, fullFilePath, src) == nil {
-		assertSuccess(t, baseDir, stat, []lint.Rule{rule}, c)
+	ins := parseInstructions(t, fullFilePath, src)
+	if ins == nil {
+		assertSuccess(t, fullFilePath, []lint.Rule{rule}, c)
 		return
 	}
-	assertFailures(t, baseDir, stat, src, []lint.Rule{rule}, c)
+	assertFailures(t, fullFilePath, []lint.Rule{rule}, c, ins)
 }
 
-func assertSuccess(t *testing.T, baseDir string, fi os.FileInfo, rules []lint.Rule, config map[string]lint.RuleConfig) error {
+func assertSuccess(t *testing.T, filePath string, rules []lint.Rule, config map[string]lint.RuleConfig) error {
 	t.Helper()
 
 	l := lint.New(os.ReadFile, 0)
 
-	filePath := filepath.Join(baseDir, fi.Name())
 	ps, err := l.Lint([][]string{{filePath}}, rules, lint.Config{
 		Rules: config,
 	})
@@ -84,14 +81,12 @@ func assertSuccess(t *testing.T, baseDir string, fi os.FileInfo, rules []lint.Ru
 	return nil
 }
 
-func assertFailures(t *testing.T, baseDir string, fi os.FileInfo, src []byte, rules []lint.Rule, config map[string]lint.RuleConfig) error {
+func assertFailures(t *testing.T, filePath string, rules []lint.Rule, config map[string]lint.RuleConfig, ins []instruction) error {
 	t.Helper()
 
 	l := lint.New(os.ReadFile, 0)
 
-	ins := parseInstructions(t, filepath.Join(baseDir, fi.Name()), src)
-
-	ps, err := l.Lint([][]string{{filepath.Join(baseDir, fi.Name())}}, rules, lint.Config{
+	ps, err := l.Lint([][]string{{filePath}}, rules, lint.Config{
 		Rules: config,
 	})
 	if err != nil {
@@ -119,13 +114,13 @@ func assertFailures(t *testing.T, baseDir string, fi os.FileInfo, src []byte, ru
 						r = r[:i]
 					}
 					if r != in.Replacement {
-						t.Errorf("Lint failed at %s:%d; got replacement %q, want %q", fi.Name(), in.Line, r, in.Replacement)
+						t.Errorf("Lint failed at %s:%d; got replacement %q, want %q", filePath, in.Line, r, in.Replacement)
 					}
 				}
 
 				if in.Confidence > 0 {
 					if in.Confidence != p.Confidence {
-						t.Errorf("Lint failed at %s:%d; got confidence %f, want %f", fi.Name(), in.Line, p.Confidence, in.Confidence)
+						t.Errorf("Lint failed at %s:%d; got confidence %f, want %f", filePath, in.Line, p.Confidence, in.Confidence)
 					}
 				}
 
@@ -138,11 +133,11 @@ func assertFailures(t *testing.T, baseDir string, fi os.FileInfo, src []byte, ru
 			}
 		}
 		if !ok {
-			t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi.Name(), in.Line, in.Match)
+			t.Errorf("Lint failed at %s:%d; /%v/ did not match", filePath, in.Line, in.Match)
 		}
 	}
 	for _, p := range failures {
-		t.Errorf("Unexpected problem at %s:%d: %v", fi.Name(), p.Position.Start.Line, p.Failure)
+		t.Errorf("Unexpected problem at %s:%d: %v", filePath, p.Position.Start.Line, p.Failure)
 	}
 	return nil
 }
diff --git a/testdata/golint/error_naming.go b/testdata/golint/error_naming.go
index e8c7bbf..cda1a62 100644
--- a/testdata/golint/error_naming.go
+++ b/testdata/golint/error_naming.go
@@ -19,9 +19,19 @@ var (
 	e1 = fmt.Errorf("blah %d", 4) // MATCH /error var e1 should have name of the form errFoo/
 	// E2 ...
 	E2 = fmt.Errorf("blah %d", 5) // MATCH /error var E2 should have name of the form ErrFoo/
+
+	// check there is no false positive for blank identifier.
+	// This pattern that can be found in benchmarks and examples should be allowed.
+	// The fact that the error variable is not used is out of the scope of this rule.
+	_ = errors.New("ok")
 )
 
 func f() {
-	var whatever = errors.New("ok") // ok
+
+	// the linter does not check local variables, this one is valid
+	var whatever = errors.New("ok")
 	_ = whatever
+
+	// same as above with a blank identifier
+	_ = errors.New("ok")
 }
diff --git a/testdata/time_date_decimal_literal.go b/testdata/time_date_decimal_literal.go
index d2b9526..f63ad39 100644
--- a/testdata/time_date_decimal_literal.go
+++ b/testdata/time_date_decimal_literal.go
@@ -71,6 +71,15 @@ var (
 		time.UTC)
 )
 
+// here we are checking that we also detect non-decimal notation in methods/functions/lambdas
+func _() {
+	_ = time.Date(2023, 01, 2, 3, 4, 5, 6, time.UTC) // MATCH /use decimal digits for time.Date month argument: octal notation with leading zero found: use 1 instead of 01/
+
+	_ = func() time.Time {
+		return time.Date(2023, 01, 2, 3, 4, 5, 6, time.UTC) // MATCH /use decimal digits for time.Date month argument: octal notation with leading zero found: use 1 instead of 01/
+	}
+}
+
 // these should never match
 var (
 	_ = time.Date(2023, 1, 2, 3, 4, 5, 1234567, time.UTC)
diff --git a/testdata/unnecessary_format.go b/testdata/unnecessary_format.go
new file mode 100644
index 0000000..f19a303
--- /dev/null
+++ b/testdata/unnecessary_format.go
@@ -0,0 +1,81 @@
+package fixtures
+
+import (
+	"fmt"
+	"log"
+	"runtime/trace"
+	"testing"
+)
+
+func unnecessaryFormat(t *testing.T, b *testing.B, f *testing.F) {
+	logger := log.New(nil, "", 0)
+
+	fmt.Appendf(nil, "no format") // MATCH /unnecessary use of formatting function "fmt.Appendf", you can replace it with "fmt.Append"/
+	fmt.Errorf("no format")       // MATCH /unnecessary use of formatting function "fmt.Errorf", you can replace it with "errors.New"/
+	fmt.Fprintf(nil, "no format") // MATCH /unnecessary use of formatting function "fmt.Fprintf", you can replace it with "fmt.Fprint"/
+	fmt.Fscanf(nil, "no format")  // MATCH /unnecessary use of formatting function "fmt.Fscanf", you can replace it with "fmt.Fscan" or "fmt.Fscanln"/
+	fmt.Printf("no format")       // MATCH /unnecessary use of formatting function "fmt.Printf", you can replace it with "fmt.Print" or "fmt.Println"/
+	fmt.Scanf("no format")        // MATCH /unnecessary use of formatting function "fmt.Scanf", you can replace it with "fmt.Scan"/
+	fmt.Sprintf("no format")      // MATCH /unnecessary use of formatting function "fmt.Sprintf", you can replace it with "fmt.Sprint" or just the string itself"/
+	fmt.Sscanf("", "no format")   // MATCH /unnecessary use of formatting function "fmt.Sscanf", you can replace it with "fmt.Sscan"/
+	// standard logging functions
+	log.Fatalf("no format")    // MATCH /unnecessary use of formatting function "log.Fatalf", you can replace it with "log.Fatal"/
+	log.Panicf("no format")    // MATCH /unnecessary use of formatting function "log.Panicf", you can replace it with "log.Panic"/
+	log.Printf("no format")    // MATCH /unnecessary use of formatting function "log.Printf", you can replace it with "log.Print"/
+	logger.Fatalf("no format") // MATCH /unnecessary use of formatting function "logger.Fatalf", you can replace it with "logger.Fatal"/
+	logger.Panicf("no format") // MATCH /unnecessary use of formatting function "logger.Panicf", you can replace it with "logger.Panic"/
+	logger.Printf("no format") // MATCH /unnecessary use of formatting function "logger.Printf", you can replace it with "logger.Print"/
+	// standard testing functions
+	t.Errorf("no format") // MATCH /unnecessary use of formatting function "t.Errorf", you can replace it with "t.Error"/
+	t.Fatalf("no format") // MATCH /unnecessary use of formatting function "t.Fatalf", you can replace it with "t.Fatal"/
+	t.Logf("no format")   // MATCH /unnecessary use of formatting function "t.Logf", you can replace it with "t.Log"/
+	t.Skipf("no format")  // MATCH /unnecessary use of formatting function "t.Skipf", you can replace it with "t.Skip"/
+	b.Errorf("no format") // MATCH /unnecessary use of formatting function "b.Errorf", you can replace it with "b.Error"/
+	b.Fatalf("no format") // MATCH /unnecessary use of formatting function "b.Fatalf", you can replace it with "b.Fatal"/
+	b.Logf("no format")   // MATCH /unnecessary use of formatting function "b.Logf", you can replace it with "b.Log"/
+	b.Skipf("no format")  // MATCH /unnecessary use of formatting function "b.Skipf", you can replace it with "b.Skip"/
+	f.Errorf("no format") // MATCH /unnecessary use of formatting function "f.Errorf", you can replace it with "f.Error"/
+	f.Fatalf("no format") // MATCH /unnecessary use of formatting function "f.Fatalf", you can replace it with "f.Fatal"/
+	f.Logf("no format")   // MATCH /unnecessary use of formatting function "f.Logf", you can replace it with "f.Log"/
+	f.Skipf("no format")  // MATCH /unnecessary use of formatting function "f.Skipf", you can replace it with "f.Skip"/
+	// standard trace functions
+	trace.Logf(nil, "http", "no format", nil) // MATCH /unnecessary use of formatting function "trace.Logf", you can replace it with "trace.Log"/
+
+	fmt.Appendf(nil, "format %d", 0)
+	fmt.Errorf("format %d", 0)
+	fmt.Fprintf(nil, "format %d", 0)
+	fmt.Fscanf(nil, "format %d", 0)
+	fmt.Printf("format %d", 0)
+	fmt.Scanf("format %d", 0)
+	fmt.Sprintf("format %d", 0)
+	fmt.Sscanf("", "format %d", 0)
+	// standard logging functions
+	log.Fatalf("format %d", 0)
+	log.Panicf("format %d", 0)
+	log.Printf("format %d", 0)
+	logger.Fatalf("format %d", 0)
+	logger.Panicf("format %d", 0)
+	logger.Printf("format %d", 0)
+	// standard testing functions
+	t.Errorf("format %d", 0)
+	t.Fatalf("format %d", 0)
+	t.Logf("format %d", 0)
+	t.Skipf("format %d", 0)
+	b.Errorf("format %d", 0)
+	b.Fatalf("format %d", 0)
+	b.Logf("format %d", 0)
+	b.Skipf("format %d", 0)
+	f.Errorf("format %d", 0)
+	f.Fatalf("format %d", 0)
+	f.Logf("format %d", 0)
+	f.Skipf("format %d", 0)
+	// standard trace functions
+	trace.Logf(nil, "http", "format %d", nil)
+
+	// test with multiline string argument
+	// MATCH:77 /unnecessary use of formatting function "fmt.Appendf", you can replace it with "fmt.Append"/
+	fmt.Appendf(nil, `no 
+		format`)
+	fmt.Appendf(nil, `format 
+	%d`, 0)
+}