Repository: pantsbuild/pants

Issue:
Go: support passthrough args for `gofmt`
The options:

```
usage: gofmt [flags] [path ...]
  -cpuprofile string
    	write cpu profile to this file
  -d	display diffs instead of rewriting files
  -e	report all errors (not just the first 10 on different lines)
  -l	list files whose formatting differs from gofmt's
  -r string
    	rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
  -s	simplify code
  -w	write result to (
```

`-e`, `-r`, and `-s` are useful to let users set. But the others probably should not be allowed. Two ways to model that:

a) having an --args option and validating -w etc are not set
b) having dedicated options like --gofmt-simplify

I suspect the `--args` option is the most idiomatic for Pants.


Discussion:
Note that there is no config file for Go. No need to support that.

Resolving patch:
diff --git a/src/python/pants/backend/go/lint/gofmt/rules.py b/src/python/pants/backend/go/lint/gofmt/rules.py
--- a/src/python/pants/backend/go/lint/gofmt/rules.py
+++ b/src/python/pants/backend/go/lint/gofmt/rules.py
@@ -7,7 +7,11 @@
 from dataclasses import dataclass
 
 from pants.backend.go.lint.gofmt.skip_field import SkipGofmtField
-from pants.backend.go.lint.gofmt.subsystem import GofmtSubsystem
+from pants.backend.go.lint.gofmt.subsystem import (
+    SUPPORTED_GOFMT_ARGS,
+    SUPPORTED_GOFMT_ARGS_AS_HELP,
+    GofmtSubsystem,
+)
 from pants.backend.go.target_types import GoPackageSourcesField
 from pants.backend.go.util_rules import goroot
 from pants.backend.go.util_rules.goroot import GoRoot
@@ -38,11 +42,27 @@ class GofmtRequest(FmtTargetsRequest):
     partitioner_type = PartitionerType.DEFAULT_SINGLE_PARTITION
 
 
+class GoFmtUnsupportedArgsPassedError(Exception):
+    pass
+
+
+async def _validate_gofmt_args(args: tuple[str, ...]):
+    """Validate that args passed to the gofmt are supported."""
+    if not set(args).issubset(SUPPORTED_GOFMT_ARGS):
+        raise GoFmtUnsupportedArgsPassedError(
+            f"Only the following style related options are supported: {SUPPORTED_GOFMT_ARGS_AS_HELP}."
+        )
+
+
 @rule(desc="Format with gofmt")
-async def gofmt_fmt(request: GofmtRequest.Batch, goroot: GoRoot) -> FmtResult:
+async def gofmt_fmt(
+    request: GofmtRequest.Batch, gofmt: GofmtSubsystem, goroot: GoRoot
+) -> FmtResult:
+    await _validate_gofmt_args(gofmt.args)
     argv = (
         os.path.join(goroot.path, "bin/gofmt"),
         "-w",
+        *gofmt.args,
         # Filter out non-.go files, e.g. assembly sources, from the file list.
         *(f for f in request.files if f.endswith(".go")),
     )
diff --git a/src/python/pants/backend/go/lint/gofmt/subsystem.py b/src/python/pants/backend/go/lint/gofmt/subsystem.py
--- a/src/python/pants/backend/go/lint/gofmt/subsystem.py
+++ b/src/python/pants/backend/go/lint/gofmt/subsystem.py
@@ -1,8 +1,12 @@
 # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
-from pants.option.option_types import SkipOption
+from pants.option.option_types import ArgsListOption, SkipOption
 from pants.option.subsystem import Subsystem
+from pants.util.ordered_set import FrozenOrderedSet
+
+SUPPORTED_GOFMT_ARGS = FrozenOrderedSet(("-e", "-r", "-s"))
+SUPPORTED_GOFMT_ARGS_AS_HELP = ", ".join([f"`{arg}`" for arg in SUPPORTED_GOFMT_ARGS])
 
 
 class GofmtSubsystem(Subsystem):
@@ -11,3 +15,7 @@ class GofmtSubsystem(Subsystem):
     help = "Gofmt-specific options."
 
     skip = SkipOption("fmt", "lint")
+    args = ArgsListOption(
+        example="-s -e",
+        extra_help=f"Only the following style related options are supported: {SUPPORTED_GOFMT_ARGS_AS_HELP}.",
+    )


Test patch:
diff --git a/src/python/pants/backend/go/lint/gofmt/rules_integration_test.py b/src/python/pants/backend/go/lint/gofmt/rules_integration_test.py
--- a/src/python/pants/backend/go/lint/gofmt/rules_integration_test.py
+++ b/src/python/pants/backend/go/lint/gofmt/rules_integration_test.py
@@ -10,6 +10,7 @@
 from pants.backend.go import target_type_rules
 from pants.backend.go.lint.gofmt.rules import GofmtFieldSet, GofmtRequest
 from pants.backend.go.lint.gofmt.rules import rules as gofmt_rules
+from pants.backend.go.lint.gofmt.subsystem import SUPPORTED_GOFMT_ARGS_AS_HELP
 from pants.backend.go.target_types import GoModTarget, GoPackageTarget
 from pants.backend.go.util_rules import (
     assembly,
@@ -26,6 +27,7 @@
 from pants.engine.addresses import Address
 from pants.engine.fs import CreateDigest, Digest, FileContent
 from pants.engine.internals.native_engine import Snapshot
+from pants.engine.internals.scheduler import ExecutionError
 from pants.engine.target import Target
 from pants.testutil.rule_runner import QueryRule, RuleRunner
 
@@ -94,6 +96,33 @@ def rule_runner() -> RuleRunner:
     """
 )
 
+BAD_FILE_TO_SIMPLIFY = dedent(
+    """\
+    package grok
+
+    import (
+    \t"fmt"
+    )
+
+    func Grok(s string) {
+    \tfmt.Println(s[1:len(s)])
+    }
+    """
+)
+
+FIXED_BAD_FILE_TO_SIMPLIFY = dedent(
+    """\
+    package grok
+
+    import (
+    \t"fmt"
+    )
+
+    func Grok(s string) {
+    \tfmt.Println(s[1:])
+    }
+    """
+)
 
 GO_MOD = dedent(
     """\
@@ -148,6 +177,26 @@ def test_passing(rule_runner: RuleRunner) -> None:
     assert fmt_result.did_change is False
 
 
+def test_failing_gofmt_flags(rule_runner: RuleRunner) -> None:
+    rule_runner.write_files(
+        {
+            "f.go": BAD_FILE_TO_SIMPLIFY,
+            "go.mod": GO_MOD,
+            "BUILD": "go_mod(name='mod')\ngo_package(name='pkg')",
+        }
+    )
+    tgt = rule_runner.get_target(Address("", target_name="pkg"))
+    with pytest.raises(ExecutionError, match=SUPPORTED_GOFMT_ARGS_AS_HELP):
+        run_gofmt(rule_runner, [tgt], extra_args=["--gofmt-args=-unsupported"])
+
+    fmt_result = run_gofmt(
+        rule_runner, [tgt], extra_args=["--gofmt-args=-s"]
+    )  # -s flag will simplify the code
+    assert fmt_result.stderr == ""
+    assert fmt_result.output == get_snapshot(rule_runner, {"f.go": FIXED_BAD_FILE_TO_SIMPLIFY})
+    assert fmt_result.did_change is True
+
+
 def test_failing(rule_runner: RuleRunner) -> None:
     rule_runner.write_files(
         {"f.go": BAD_FILE, "go.mod": GO_MOD, "BUILD": "go_mod(name='mod')\ngo_package(name='pkg')"}