Skip to content

Commit a5b6b6f

Browse files
borkaehwittaiz
authored andcommitted
Phase Scalafmt (#912)
* Phase Scalafmt * Reuse formatter * Remove glob * Remove rules_jvm_external * Rename argparse * Remove executable * Use shared code * Remove imports * Add comment * Change file name * Move args to private function * Change to true * Change conf location * Change default conf * Test custom conf * Fix lint * Fix build * Remove trailing commas * Add formatted and unformatted folder * Rename test function * Remove template file * Better handle failing case * Drop argparser * Remove resolve_command * Add comments * Remove unnecessary code * Change to RUNPATH * Rename gitignore backup * Remove comment * Move conf file * Add doc to attribute * Switch to match readme * Add doc * Move doc to separate md * Fix wording * Fix lint * Add url * Add url
1 parent d2e7e3b commit a5b6b6f

26 files changed

+824
-1
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ hash2
77
.bazel_cache
88
.ijwb
99
.metals
10+
unformatted-*.backup.scala

.scalafmt.conf

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
align.openParenCallSite = false
2+
align.openParenDefnSite = false
3+
continuationIndent.defnSite = 2
4+
danglingParentheses = true
5+
docstrings = JavaDoc
6+
importSelectors = singleLine
7+
maxColumn = 120
8+
verticalMultiline.newlineBeforeImplicitKW = true
9+
rewrite.redundantBraces.stringInterpolation = true
10+
rewrite.rules = [
11+
RedundantParens,
12+
PreferCurlyFors,
13+
SortImports
14+
]
15+
unindentTopLevelOperators = false

BUILD

Whitespace-only changes.

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ Phases provide 3 major benefits:
201201

202202
See [Customizable Phase](docs/customizable_phase.md) for more info.
203203

204+
### Phase extensions
205+
- [Scala Format](docs/phase_scalafmt.md)
206+
204207
## Building from source
205208
Test & Build:
206209
```

WORKSPACE

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ load("//specs2:specs2_junit.bzl", "specs2_junit_repositories")
4040

4141
specs2_junit_repositories()
4242

43+
load("//scala/scalafmt:scalafmt_repositories.bzl", "scalafmt_default_config", "scalafmt_repositories")
44+
45+
scalafmt_default_config()
46+
47+
scalafmt_repositories()
48+
4349
load("//scala:scala_cross_version.bzl", "default_scala_major_version", "scala_mvn_artifact")
4450

4551
MAVEN_SERVER_URLS = [

docs/phase_scalafmt.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Phase Scalafmt
2+
3+
## Contents
4+
* [Overview](#overview)
5+
* [How to set up](#how-to-set-up)
6+
7+
## Overview
8+
A phase extension `phase_scalafmt` can format Scala source code via [Scalafmt](https://scalameta.org/scalafmt/).
9+
10+
## How to set up
11+
Add this snippet to `WORKSPACE`
12+
```
13+
load("//scala/scalafmt:scalafmt_repositories.bzl", "scalafmt_default_config", "scalafmt_repositories")
14+
scalafmt_default_config()
15+
scalafmt_repositories()
16+
```
17+
18+
To add this phase to a rule, you have to pass the extension to a rule macro. Take `scala_binary` for example,
19+
```
20+
load("//scala:advanced_usage/scala.bzl", "make_scala_binary")
21+
load("//scala/scalafmt:phase_scalafmt_ext.bzl", "ext_scalafmt")
22+
23+
scalafmt_scala_binary = make_scala_binary(ext_scalafmt)
24+
```
25+
Then use `scalafmt_scala_binary` as normal.
26+
27+
The extension adds 2 additional attributes to the rule
28+
- `format`: enable formatting
29+
- `config`: the Scalafmt configuration file
30+
31+
When `format` is set to `true`, you can do
32+
```
33+
bazel run <TARGET>.format
34+
```
35+
to format the source code, and do
36+
```
37+
bazel run <TARGET>.format-test
38+
```
39+
to check the format (without modifying source code).
40+
41+
The extension provides default configuration, but there are 2 ways to use custom configuration
42+
- Put `.scalafmt.conf` at root of your workspace
43+
- Pass `.scalafmt.conf` in via `config` attribute

scala/private/macros/scala_repositories.bzl

+4-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ def scala_repositories(
5656
_default_scala_version(),
5757
_default_scala_version_jar_shas(),
5858
),
59-
maven_servers = ["https://repo.maven.apache.org/maven2"],
59+
maven_servers = [
60+
"https://repo.maven.apache.org/maven2",
61+
"https://maven-central.storage-download.googleapis.com/maven2",
62+
],
6063
scala_extra_jars = _default_scala_extra_jars()):
6164
(scala_version, scala_version_jar_shas) = scala_version_shas
6265
major_version = _extract_major_version(scala_version)
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#
2+
# PHASE: phase scalafmt
3+
#
4+
# Outputs to format the scala files when it is explicitly specified
5+
#
6+
def phase_scalafmt(ctx, p):
7+
if ctx.attr.format:
8+
manifest, files = _build_format(ctx)
9+
_formatter(ctx, manifest, files, ctx.file._runner, ctx.outputs.scalafmt_runner)
10+
_formatter(ctx, manifest, files, ctx.file._testrunner, ctx.outputs.scalafmt_testrunner)
11+
else:
12+
_write_empty_content(ctx, ctx.outputs.scalafmt_runner)
13+
_write_empty_content(ctx, ctx.outputs.scalafmt_testrunner)
14+
15+
def _build_format(ctx):
16+
files = []
17+
manifest_content = []
18+
for src in ctx.files.srcs:
19+
# only format scala source files, not generated files
20+
if src.path.endswith(".scala") and src.is_source:
21+
file = ctx.actions.declare_file("{}.fmt.output".format(src.short_path))
22+
files.append(file)
23+
ctx.actions.run(
24+
arguments = ["--jvm_flag=-Dfile.encoding=UTF-8", _format_args(ctx, src, file)],
25+
executable = ctx.executable._fmt,
26+
outputs = [file],
27+
inputs = [ctx.file.config, src],
28+
execution_requirements = {"supports-workers": "1"},
29+
mnemonic = "ScalaFmt",
30+
)
31+
manifest_content.append("{} {}".format(src.short_path, file.short_path))
32+
33+
# record the source path and the formatted file path
34+
# so that we know where to copy the formatted file to replace the source file
35+
manifest = ctx.actions.declare_file("format/{}/manifest.txt".format(ctx.label.name))
36+
ctx.actions.write(manifest, "\n".join(manifest_content) + "\n")
37+
38+
return manifest, files
39+
40+
def _formatter(ctx, manifest, files, template, output_runner):
41+
ctx.actions.run_shell(
42+
inputs = [template, manifest] + files,
43+
outputs = [output_runner],
44+
# replace %workspace% and %manifest% in template and rewrite it to output_runner
45+
command = "cat $1 | sed -e s#%workspace%#$2# -e s#%manifest%#$3# > $4",
46+
arguments = [
47+
template.path,
48+
ctx.workspace_name,
49+
manifest.short_path,
50+
output_runner.path,
51+
],
52+
execution_requirements = {},
53+
)
54+
55+
def _write_empty_content(ctx, output_runner):
56+
ctx.actions.write(
57+
output = output_runner,
58+
content = "",
59+
)
60+
61+
def _format_args(ctx, src, file):
62+
args = ctx.actions.args()
63+
args.add(ctx.file.config.path)
64+
args.add(src.path)
65+
args.add(file.path)
66+
args.set_param_file_format("multiline")
67+
args.use_param_file("@%s", use_always = True)
68+
return args

scala/private/phases/phases.bzl

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ load("@io_bazel_rules_scala//scala/private:phases/phase_declare_executable.bzl",
6060
load("@io_bazel_rules_scala//scala/private:phases/phase_merge_jars.bzl", _phase_merge_jars = "phase_merge_jars")
6161
load("@io_bazel_rules_scala//scala/private:phases/phase_jvm_flags.bzl", _phase_jvm_flags = "phase_jvm_flags")
6262
load("@io_bazel_rules_scala//scala/private:phases/phase_coverage_runfiles.bzl", _phase_coverage_runfiles = "phase_coverage_runfiles")
63+
load("@io_bazel_rules_scala//scala/private:phases/phase_scalafmt.bzl", _phase_scalafmt = "phase_scalafmt")
6364

6465
# API
6566
run_phases = _run_phases
@@ -129,3 +130,6 @@ phase_runfiles_common = _phase_runfiles_common
129130
phase_default_info_binary = _phase_default_info_binary
130131
phase_default_info_library = _phase_default_info_library
131132
phase_default_info_scalatest = _phase_default_info_scalatest
133+
134+
# scalafmt
135+
phase_scalafmt = _phase_scalafmt

scala/scalafmt/BUILD

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
load("//scala:scala.bzl", "scala_binary")
2+
3+
filegroup(
4+
name = "runner",
5+
srcs = ["private/format.template.sh"],
6+
visibility = ["//visibility:public"],
7+
)
8+
9+
filegroup(
10+
name = "testrunner",
11+
srcs = ["private/format-test.template.sh"],
12+
visibility = ["//visibility:public"],
13+
)
14+
15+
scala_binary(
16+
name = "scalafmt",
17+
srcs = ["scalafmt/ScalafmtRunner.scala"],
18+
main_class = "io.bazel.rules_scala.scalafmt.ScalafmtRunner",
19+
visibility = ["//visibility:public"],
20+
deps = [
21+
"//src/java/io/bazel/rulesscala/worker",
22+
"@com_geirsson_metaconfig_core_2_11",
23+
"@org_scalameta_parsers_2_11",
24+
"@org_scalameta_scalafmt_core_2_11",
25+
],
26+
)
27+
28+
load(
29+
"//scala/scalafmt:phase_scalafmt_ext.bzl",
30+
"scalafmt_singleton",
31+
)
32+
33+
scalafmt_singleton(
34+
name = "phase_scalafmt",
35+
visibility = ["//visibility:public"],
36+
)

scala/scalafmt/phase_scalafmt_ext.bzl

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
load(
2+
"//scala:advanced_usage/providers.bzl",
3+
_ScalaRulePhase = "ScalaRulePhase",
4+
)
5+
load(
6+
"//scala/private:phases/phases.bzl",
7+
_phase_scalafmt = "phase_scalafmt",
8+
)
9+
10+
ext_scalafmt = {
11+
"attrs": {
12+
"config": attr.label(
13+
allow_single_file = [".conf"],
14+
default = "@scalafmt_default//:config",
15+
doc = "The Scalafmt configuration file.",
16+
),
17+
"format": attr.bool(
18+
default = False,
19+
doc = "Switch of enabling formatting.",
20+
),
21+
"_fmt": attr.label(
22+
cfg = "host",
23+
default = "//scala/scalafmt",
24+
executable = True,
25+
),
26+
"_runner": attr.label(
27+
allow_single_file = True,
28+
default = "//scala/scalafmt:runner",
29+
),
30+
"_testrunner": attr.label(
31+
allow_single_file = True,
32+
default = "//scala/scalafmt:testrunner",
33+
),
34+
},
35+
"outputs": {
36+
"scalafmt_runner": "%{name}.format",
37+
"scalafmt_testrunner": "%{name}.format-test",
38+
},
39+
"phase_providers": [
40+
"//scala/scalafmt:phase_scalafmt",
41+
],
42+
}
43+
44+
def _scalafmt_singleton_implementation(ctx):
45+
return [
46+
_ScalaRulePhase(
47+
custom_phases = [
48+
("$", "", "scalafmt", _phase_scalafmt),
49+
],
50+
),
51+
]
52+
53+
scalafmt_singleton = rule(
54+
implementation = _scalafmt_singleton_implementation,
55+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash -e
2+
WORKSPACE_ROOT="${1:-$BUILD_WORKSPACE_DIRECTORY}"
3+
RUNPATH="${TEST_SRCDIR-$0.runfiles}"/%workspace%
4+
RUNPATH=(${RUNPATH//bin/ })
5+
RUNPATH="${RUNPATH[0]}"bin
6+
7+
EXIT=0
8+
while read original formatted; do
9+
if [[ ! -z "$original" ]] && [[ ! -z "$formatted" ]]; then
10+
if ! cmp -s "$WORKSPACE_ROOT/$original" "$RUNPATH/$formatted"; then
11+
echo $original
12+
diff "$WORKSPACE_ROOT/$original" "$RUNPATH/$formatted" || true
13+
EXIT=1
14+
fi
15+
fi
16+
done < "$RUNPATH"/%manifest%
17+
18+
exit $EXIT
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash -e
2+
WORKSPACE_ROOT="${1:-$BUILD_WORKSPACE_DIRECTORY}"
3+
RUNPATH="${TEST_SRCDIR-$0.runfiles}"/%workspace%
4+
RUNPATH=(${RUNPATH//bin/ })
5+
RUNPATH="${RUNPATH[0]}"bin
6+
7+
while read original formatted; do
8+
if [[ ! -z "$original" ]] && [[ ! -z "$formatted" ]]; then
9+
if ! cmp -s "$WORKSPACE_ROOT/$original" "$RUNPATH/$formatted"; then
10+
echo "Formatting $original"
11+
cp "$RUNPATH/$formatted" "$WORKSPACE_ROOT/$original"
12+
fi
13+
fi
14+
done < "$RUNPATH"/%manifest%
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.bazel.rules_scala.scalafmt
2+
3+
import io.bazel.rulesscala.worker.{GenericWorker, Processor};
4+
import java.io.File
5+
import java.nio.file.Files
6+
import org.scalafmt.Scalafmt
7+
import org.scalafmt.config.Config
8+
import org.scalafmt.util.FileOps
9+
import scala.annotation.tailrec
10+
import scala.collection.JavaConverters._
11+
import scala.io.Codec
12+
13+
object ScalafmtRunner extends GenericWorker(new ScalafmtProcessor) {
14+
def main(args: Array[String]) {
15+
try run(args)
16+
catch {
17+
case x: Exception =>
18+
x.printStackTrace()
19+
System.exit(1)
20+
}
21+
}
22+
}
23+
24+
class ScalafmtProcessor extends Processor {
25+
def processRequest(args: java.util.List[String]) {
26+
val argName = List("config", "input", "output")
27+
val argFile = args.asScala.map{x => new File(x)}
28+
val namespace = argName.zip(argFile).toMap
29+
30+
val source = FileOps.readFile(namespace.getOrElse("input", new File("")))(Codec.UTF8)
31+
32+
val config = Config.fromHoconFile(namespace.getOrElse("config", new File(""))).get
33+
@tailrec
34+
def format(code: String): String = {
35+
val formatted = Scalafmt.format(code, config).get
36+
if (code == formatted) code else format(formatted)
37+
}
38+
39+
val output = try {
40+
format(source)
41+
} catch {
42+
case e @ (_: org.scalafmt.Error | _: scala.meta.parsers.ParseException) => {
43+
System.out.println("Unable to format file due to bug in scalafmt")
44+
System.out.println(e.toString)
45+
source
46+
}
47+
}
48+
49+
Files.write(namespace.getOrElse("output", new File("")).toPath, output.getBytes)
50+
}
51+
}

0 commit comments

Comments
 (0)