Repository: golangci/golangci-lint (Go)

Issue:
Output report paths specified in the configuration file should be relative to that configuration file
### Welcome

- [X] Yes, I'm using a binary release within 2 latest major releases. Only such installations are supported.
- [X] Yes, I've searched similar issues on GitHub and didn't find any.
- [X] Yes, I've included all information below (version, config, etc.).
- [X] Yes, I've tried with the standalone linter if available (e.g., gocritic, go vet, etc.). (https://golangci-lint.run/usage/linters/)

### Description of the problem

The path for the output report specified in a configuration file is apparently relative to the current working directory that golangci-lint is invoked from, rather than relative to the configuration file itself.  This is a problem if the configuration file is located in a parent directory.

**Steps to reproduce**

1. Create a configuration file at `~/Thumbtack/go/src/github.com/thumbtack/go/.golangci.yml` that includes the following:
```
output:
  format: colored-line-number,junit-xml:reports/junit/lint.xml
```
2. Create a directory at `~/Thumbtack/go/src/github.com/thumbtack/go/reports/junit` that will hold the report output. (The directory creation as a first step is necessary because this has not been implemented: https://github.com/golangci/golangci-lint/issues/3623)
3. Change directory to a sub-package, and then run the linter:
```
% cd ~/Thumbtack/go/src/github.com/thumbtack/go/lib/system
% pwd
/Users/james.johnston/Thumbtack/go/src/github.com/thumbtack/go/lib/system
% golangci-lint --verbose run ./...
```

**Expected behavior**

The linter should write a report file at `~/Thumbtack/go/src/github.com/thumbtack/go/reports/junit/lint.xml` because that is the output path specified in the configuration file (relative to the directory containing the configuration file).

**Actual behavior**

The linter fails with an error, because it tries to write the output in a location relative to the current working directory (`lib/system` sub-directory), and does not find a pre-existing output directory there:

```
INFO [config_reader] Config search paths: [./ /Users/james.johnston/Thumbtack/go/src/github.com/thumbtack/go/lib/system /Users/james.johnston/Thumbtack/go/src/github.com/thumbtack/go/lib /Users/james.johnston/Thumbtack/go/src/github.com/thumbtack/go /Users/james.johnston/Thumbtack/go/src/github.com/thumbtack /Users/james.johnston/Thumbtack/go/src/github.com /Users/james.johnston/Thumbtack/go/src /Users/james.johnston/Thumbtack/go /Users/james.johnston/Thumbtack /Users/james.johnston /Users /]
INFO [config_reader] Used config file ../../.golangci.yml
INFO [lintersdb] Active 16 linters: [bodyclose errcheck errorlint gocritic gosec gosimple govet ineffassign noctx reassign revive scopelint staticcheck stylecheck typecheck unused]
INFO [loader] Go packages loading at mode 575 (imports|types_sizes|compiled_files|deps|files|exports_file|name) took 141.1195ms
WARN [runner] The linter 'scopelint' is deprecated (since v1.39.0) due to: The repository of the linter has been deprecated by the owner.  Replaced by exportloopref.
INFO [runner/filename_unadjuster] Pre-built 0 adjustments in 1.335042ms
INFO [linters_context/goanalysis] analyzers took 0s with no stages
INFO [runner] Issues before processing: 30, after processing: 0
INFO [runner] Processors filtering stat (out/in): uniq_by_line: 21/24, path_prettifier: 30/30, exclude-rules: 24/28, nolint: 24/24, cgo: 30/30, exclude: 28/28, skip_dirs: 30/30, autogenerated_exclude: 28/30, diff: 0/21, filename_unadjuster: 30/30, skip_files: 30/30, identifier_marker: 28/28
INFO [runner] processing took 250.08712ms with stages: diff: 247.169708ms, nolint: 1.074416ms, exclude-rules: 676.333µs, autogenerated_exclude: 481.041µs, identifier_marker: 369.041µs, path_prettifier: 299.292µs, skip_dirs: 8.041µs, uniq_by_line: 3.375µs, cgo: 1.666µs, filename_unadjuster: 1.25µs, max_same_issues: 1.082µs, sort_results: 416ns, skip_files: 250ns, source_code: 250ns, path_shortener: 250ns, max_per_file_from_linter: 209ns, exclude: 209ns, max_from_linter: 125ns, severity-rules: 84ns, path_prefixer: 82ns
INFO [runner] linters took 705.687375ms with stages: goanalysis_metalinter: 455.4205ms
ERRO Running error: can't create output for reports/junit/lint.xml: open reports/junit/lint.xml: no such file or directory
INFO Memory: 10 samples, avg is 43.7MB, max is 57.3MB
INFO Execution took 856.919584ms
```

In the above logs, note the following:

- The configuration file used is `../../.golangci.yml` relative to the current working directory.
- However, it tries to write output to `reports/junit/lint.xml` -- not `../../reports/junit/lint.xml` which would be the path relative to the configuration file.

**Thoughts on solving this**

At the end of the day, all I want to do is write the JUnit output during CI/CD to a subdirectory relative to the base of our repository / relative to the configuration file, while at the same time allow ordinary developers to at least be able to run golangci-lint from any directory without encountering an error.

Whether that is solved by addressing https://github.com/golangci/golangci-lint/issues/3623, providing a way to evaluate the output path relative to the configuration file, or some other maintainable solution is relatively inconsequential to me.  It is the combination of both of these issues that presents a problem where it's just not practical to put output files in a subdirectory.

### Version of golangci-lint

<details>

```console
$ golangci-lint --version
golangci-lint has version 1.51.0 built from 6d3f06c5 on 2023-02-02T08:24:52Z
```

</details>

The problem still exists in master branch:
1. At https://github.com/golangci/golangci-lint/blob/50901e40aeab802222c1d41e3c1e3e5e195e207a/pkg/commands/run.go#L414-L425 we can see that the output path is passed as-is as a parameter to the `printReports` function.
2. At https://github.com/golangci/golangci-lint/blob/50901e40aeab802222c1d41e3c1e3e5e195e207a/pkg/commands/run.go#L434-L435 we see that it is then passed to `createWriter` as-is.
3. Which at https://github.com/golangci/golangci-lint/blob/50901e40aeab802222c1d41e3c1e3e5e195e207a/pkg/commands/run.go#L462-L469 we can see it is then passed to `os.OpenFile` as-is.
4. At no point is the path being adjusted to compensate for the location where the path was actually specified (e.g. the directory containing the configuration file).

### Configuration file

Relevant part cited in problem description, above.

### Go environment

<details>

```console
% go version && go env
go version go1.19.5 darwin/arm64
GO111MODULE=""
GOARCH="arm64"
GOBIN=""
GOCACHE="/Users/james.johnston/Library/Caches/go-build"
GOENV="/Users/james.johnston/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="arm64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/james.johnston/Thumbtack/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/james.johnston/Thumbtack/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_arm64"
GOVCS=""
GOVERSION="go1.19.5"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/Users/james.johnston/Thumbtack/go/src/github.com/thumbtack/go/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS="-I/usr/local/include"
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-L/usr/local/lib"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/91/w0njycjx075fd3lpnbzl44_h0000gp/T/go-build709362124=/tmp/go-build -gno-record-gcc-switches -fno-common"
```

</details>

### Verbose output of running

Relevant verbose output cited in problem description.

### Code example or link to a public repository

Enough information should be provided in the problem description to reproduce it.


Discussion:

Patch:
diff --git a/pkg/commands/run.go b/pkg/commands/run.go
index 08de8f53c9fc..f389fff6d30a 100644
--- a/pkg/commands/run.go
+++ b/pkg/commands/run.go
@@ -194,7 +194,7 @@ func (c *runCommand) preRunE(_ *cobra.Command, args []string) error {
 
 	c.dbManager = dbManager
 
-	printer, err := printers.NewPrinter(c.log, &c.cfg.Output, c.reportData)
+	printer, err := printers.NewPrinter(c.log, &c.cfg.Output, c.reportData, c.cfg.GetBasePath())
 	if err != nil {
 		return err
 	}
diff --git a/pkg/printers/printer.go b/pkg/printers/printer.go
index c04648ad9220..613e4abec8f9 100644
--- a/pkg/printers/printer.go
+++ b/pkg/printers/printer.go
@@ -25,6 +25,7 @@ type issuePrinter interface {
 type Printer struct {
 	cfg        *config.Output
 	reportData *report.Data
+	basePath   string
 
 	log logutils.Log
 
@@ -33,7 +34,7 @@ type Printer struct {
 }
 
 // NewPrinter creates a new Printer.
-func NewPrinter(log logutils.Log, cfg *config.Output, reportData *report.Data) (*Printer, error) {
+func NewPrinter(log logutils.Log, cfg *config.Output, reportData *report.Data, basePath string) (*Printer, error) {
 	if log == nil {
 		return nil, errors.New("missing log argument in constructor")
 	}
@@ -47,6 +48,7 @@ func NewPrinter(log logutils.Log, cfg *config.Output, reportData *report.Data) (
 	return &Printer{
 		cfg:        cfg,
 		reportData: reportData,
+		basePath:   basePath,
 		log:        log,
 		stdOut:     logutils.StdOut,
 		stdErr:     logutils.StdErr,
@@ -98,6 +100,10 @@ func (c *Printer) createWriter(path string) (io.Writer, bool, error) {
 		return c.stdErr, false, nil
 	}
 
+	if !filepath.IsAbs(path) {
+		path = filepath.Join(c.basePath, path)
+	}
+
 	err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
 	if err != nil {
 		return nil, false, err


Test patch:
diff --git a/pkg/printers/printer_test.go b/pkg/printers/printer_test.go
index ef2e0bb02b50..f0ec639e0d33 100644
--- a/pkg/printers/printer_test.go
+++ b/pkg/printers/printer_test.go
@@ -67,7 +67,7 @@ func TestPrinter_Print_stdout(t *testing.T) {
 		t.Run(test.desc, func(t *testing.T) {
 			t.Parallel()
 
-			p, err := NewPrinter(logger, test.cfg, data)
+			p, err := NewPrinter(logger, test.cfg, data, "")
 			require.NoError(t, err)
 
 			var stdOutBuffer bytes.Buffer
@@ -106,7 +106,7 @@ func TestPrinter_Print_stderr(t *testing.T) {
 		},
 	}
 
-	p, err := NewPrinter(logger, cfg, data)
+	p, err := NewPrinter(logger, cfg, data, "")
 	require.NoError(t, err)
 
 	var stdOutBuffer bytes.Buffer
@@ -145,7 +145,7 @@ func TestPrinter_Print_file(t *testing.T) {
 		},
 	}
 
-	p, err := NewPrinter(logger, cfg, data)
+	p, err := NewPrinter(logger, cfg, data, "")
 	require.NoError(t, err)
 
 	var stdOutBuffer bytes.Buffer
@@ -197,7 +197,7 @@ func TestPrinter_Print_multiple(t *testing.T) {
 		},
 	}
 
-	p, err := NewPrinter(logger, cfg, data)
+	p, err := NewPrinter(logger, cfg, data, "")
 	require.NoError(t, err)
 
 	var stdOutBuffer bytes.Buffer