Skip to content

Commit

Permalink
Phase Scalafmt (bazelbuild#912)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
borkaehw authored and Andre Rocha committed Jul 6, 2020
1 parent 3e6b617 commit 0086b13
Show file tree
Hide file tree
Showing 26 changed files with 824 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ hash2
.bazel_cache
.ijwb
.metals
unformatted-*.backup.scala
15 changes: 15 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
align.openParenCallSite = false
align.openParenDefnSite = false
continuationIndent.defnSite = 2
danglingParentheses = true
docstrings = JavaDoc
importSelectors = singleLine
maxColumn = 120
verticalMultiline.newlineBeforeImplicitKW = true
rewrite.redundantBraces.stringInterpolation = true
rewrite.rules = [
RedundantParens,
PreferCurlyFors,
SortImports
]
unindentTopLevelOperators = false
Empty file added BUILD
Empty file.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ Phases provide 3 major benefits:

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

### Phase extensions
- [Scala Format](docs/phase_scalafmt.md)

## Building from source
Test & Build:
```
Expand Down
6 changes: 6 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ load("//specs2:specs2_junit.bzl", "specs2_junit_repositories")

specs2_junit_repositories()

load("//scala/scalafmt:scalafmt_repositories.bzl", "scalafmt_default_config", "scalafmt_repositories")

scalafmt_default_config()

scalafmt_repositories()

load("//scala:scala_cross_version.bzl", "default_scala_major_version", "scala_mvn_artifact")

MAVEN_SERVER_URLS = [
Expand Down
43 changes: 43 additions & 0 deletions docs/phase_scalafmt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Phase Scalafmt

## Contents
* [Overview](#overview)
* [How to set up](#how-to-set-up)

## Overview
A phase extension `phase_scalafmt` can format Scala source code via [Scalafmt](https://scalameta.org/scalafmt/).

## How to set up
Add this snippet to `WORKSPACE`
```
load("//scala/scalafmt:scalafmt_repositories.bzl", "scalafmt_default_config", "scalafmt_repositories")
scalafmt_default_config()
scalafmt_repositories()
```

To add this phase to a rule, you have to pass the extension to a rule macro. Take `scala_binary` for example,
```
load("//scala:advanced_usage/scala.bzl", "make_scala_binary")
load("//scala/scalafmt:phase_scalafmt_ext.bzl", "ext_scalafmt")
scalafmt_scala_binary = make_scala_binary(ext_scalafmt)
```
Then use `scalafmt_scala_binary` as normal.

The extension adds 2 additional attributes to the rule
- `format`: enable formatting
- `config`: the Scalafmt configuration file

When `format` is set to `true`, you can do
```
bazel run <TARGET>.format
```
to format the source code, and do
```
bazel run <TARGET>.format-test
```
to check the format (without modifying source code).

The extension provides default configuration, but there are 2 ways to use custom configuration
- Put `.scalafmt.conf` at root of your workspace
- Pass `.scalafmt.conf` in via `config` attribute
5 changes: 4 additions & 1 deletion scala/private/macros/scala_repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ def scala_repositories(
_default_scala_version(),
_default_scala_version_jar_shas(),
),
maven_servers = ["https://repo.maven.apache.org/maven2"],
maven_servers = [
"https://repo.maven.apache.org/maven2",
"https://maven-central.storage-download.googleapis.com/maven2",
],
scala_extra_jars = _default_scala_extra_jars()):
(scala_version, scala_version_jar_shas) = scala_version_shas
major_version = _extract_major_version(scala_version)
Expand Down
68 changes: 68 additions & 0 deletions scala/private/phases/phase_scalafmt.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#
# PHASE: phase scalafmt
#
# Outputs to format the scala files when it is explicitly specified
#
def phase_scalafmt(ctx, p):
if ctx.attr.format:
manifest, files = _build_format(ctx)
_formatter(ctx, manifest, files, ctx.file._runner, ctx.outputs.scalafmt_runner)
_formatter(ctx, manifest, files, ctx.file._testrunner, ctx.outputs.scalafmt_testrunner)
else:
_write_empty_content(ctx, ctx.outputs.scalafmt_runner)
_write_empty_content(ctx, ctx.outputs.scalafmt_testrunner)

def _build_format(ctx):
files = []
manifest_content = []
for src in ctx.files.srcs:
# only format scala source files, not generated files
if src.path.endswith(".scala") and src.is_source:
file = ctx.actions.declare_file("{}.fmt.output".format(src.short_path))
files.append(file)
ctx.actions.run(
arguments = ["--jvm_flag=-Dfile.encoding=UTF-8", _format_args(ctx, src, file)],
executable = ctx.executable._fmt,
outputs = [file],
inputs = [ctx.file.config, src],
execution_requirements = {"supports-workers": "1"},
mnemonic = "ScalaFmt",
)
manifest_content.append("{} {}".format(src.short_path, file.short_path))

# record the source path and the formatted file path
# so that we know where to copy the formatted file to replace the source file
manifest = ctx.actions.declare_file("format/{}/manifest.txt".format(ctx.label.name))
ctx.actions.write(manifest, "\n".join(manifest_content) + "\n")

return manifest, files

def _formatter(ctx, manifest, files, template, output_runner):
ctx.actions.run_shell(
inputs = [template, manifest] + files,
outputs = [output_runner],
# replace %workspace% and %manifest% in template and rewrite it to output_runner
command = "cat $1 | sed -e s#%workspace%#$2# -e s#%manifest%#$3# > $4",
arguments = [
template.path,
ctx.workspace_name,
manifest.short_path,
output_runner.path,
],
execution_requirements = {},
)

def _write_empty_content(ctx, output_runner):
ctx.actions.write(
output = output_runner,
content = "",
)

def _format_args(ctx, src, file):
args = ctx.actions.args()
args.add(ctx.file.config.path)
args.add(src.path)
args.add(file.path)
args.set_param_file_format("multiline")
args.use_param_file("@%s", use_always = True)
return args
4 changes: 4 additions & 0 deletions scala/private/phases/phases.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ load("@io_bazel_rules_scala//scala/private:phases/phase_declare_executable.bzl",
load("@io_bazel_rules_scala//scala/private:phases/phase_merge_jars.bzl", _phase_merge_jars = "phase_merge_jars")
load("@io_bazel_rules_scala//scala/private:phases/phase_jvm_flags.bzl", _phase_jvm_flags = "phase_jvm_flags")
load("@io_bazel_rules_scala//scala/private:phases/phase_coverage_runfiles.bzl", _phase_coverage_runfiles = "phase_coverage_runfiles")
load("@io_bazel_rules_scala//scala/private:phases/phase_scalafmt.bzl", _phase_scalafmt = "phase_scalafmt")

# API
run_phases = _run_phases
Expand Down Expand Up @@ -129,3 +130,6 @@ phase_runfiles_common = _phase_runfiles_common
phase_default_info_binary = _phase_default_info_binary
phase_default_info_library = _phase_default_info_library
phase_default_info_scalatest = _phase_default_info_scalatest

# scalafmt
phase_scalafmt = _phase_scalafmt
36 changes: 36 additions & 0 deletions scala/scalafmt/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
load("//scala:scala.bzl", "scala_binary")

filegroup(
name = "runner",
srcs = ["private/format.template.sh"],
visibility = ["//visibility:public"],
)

filegroup(
name = "testrunner",
srcs = ["private/format-test.template.sh"],
visibility = ["//visibility:public"],
)

scala_binary(
name = "scalafmt",
srcs = ["scalafmt/ScalafmtRunner.scala"],
main_class = "io.bazel.rules_scala.scalafmt.ScalafmtRunner",
visibility = ["//visibility:public"],
deps = [
"//src/java/io/bazel/rulesscala/worker",
"@com_geirsson_metaconfig_core_2_11",
"@org_scalameta_parsers_2_11",
"@org_scalameta_scalafmt_core_2_11",
],
)

load(
"//scala/scalafmt:phase_scalafmt_ext.bzl",
"scalafmt_singleton",
)

scalafmt_singleton(
name = "phase_scalafmt",
visibility = ["//visibility:public"],
)
55 changes: 55 additions & 0 deletions scala/scalafmt/phase_scalafmt_ext.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
load(
"//scala:advanced_usage/providers.bzl",
_ScalaRulePhase = "ScalaRulePhase",
)
load(
"//scala/private:phases/phases.bzl",
_phase_scalafmt = "phase_scalafmt",
)

ext_scalafmt = {
"attrs": {
"config": attr.label(
allow_single_file = [".conf"],
default = "@scalafmt_default//:config",
doc = "The Scalafmt configuration file.",
),
"format": attr.bool(
default = False,
doc = "Switch of enabling formatting.",
),
"_fmt": attr.label(
cfg = "host",
default = "//scala/scalafmt",
executable = True,
),
"_runner": attr.label(
allow_single_file = True,
default = "//scala/scalafmt:runner",
),
"_testrunner": attr.label(
allow_single_file = True,
default = "//scala/scalafmt:testrunner",
),
},
"outputs": {
"scalafmt_runner": "%{name}.format",
"scalafmt_testrunner": "%{name}.format-test",
},
"phase_providers": [
"//scala/scalafmt:phase_scalafmt",
],
}

def _scalafmt_singleton_implementation(ctx):
return [
_ScalaRulePhase(
custom_phases = [
("$", "", "scalafmt", _phase_scalafmt),
],
),
]

scalafmt_singleton = rule(
implementation = _scalafmt_singleton_implementation,
)
18 changes: 18 additions & 0 deletions scala/scalafmt/private/format-test.template.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash -e
WORKSPACE_ROOT="${1:-$BUILD_WORKSPACE_DIRECTORY}"
RUNPATH="${TEST_SRCDIR-$0.runfiles}"/%workspace%
RUNPATH=(${RUNPATH//bin/ })
RUNPATH="${RUNPATH[0]}"bin

EXIT=0
while read original formatted; do
if [[ ! -z "$original" ]] && [[ ! -z "$formatted" ]]; then
if ! cmp -s "$WORKSPACE_ROOT/$original" "$RUNPATH/$formatted"; then
echo $original
diff "$WORKSPACE_ROOT/$original" "$RUNPATH/$formatted" || true
EXIT=1
fi
fi
done < "$RUNPATH"/%manifest%

exit $EXIT
14 changes: 14 additions & 0 deletions scala/scalafmt/private/format.template.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash -e
WORKSPACE_ROOT="${1:-$BUILD_WORKSPACE_DIRECTORY}"
RUNPATH="${TEST_SRCDIR-$0.runfiles}"/%workspace%
RUNPATH=(${RUNPATH//bin/ })
RUNPATH="${RUNPATH[0]}"bin

while read original formatted; do
if [[ ! -z "$original" ]] && [[ ! -z "$formatted" ]]; then
if ! cmp -s "$WORKSPACE_ROOT/$original" "$RUNPATH/$formatted"; then
echo "Formatting $original"
cp "$RUNPATH/$formatted" "$WORKSPACE_ROOT/$original"
fi
fi
done < "$RUNPATH"/%manifest%
51 changes: 51 additions & 0 deletions scala/scalafmt/scalafmt/ScalafmtRunner.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.bazel.rules_scala.scalafmt

import io.bazel.rulesscala.worker.{GenericWorker, Processor};
import java.io.File
import java.nio.file.Files
import org.scalafmt.Scalafmt
import org.scalafmt.config.Config
import org.scalafmt.util.FileOps
import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.io.Codec

object ScalafmtRunner extends GenericWorker(new ScalafmtProcessor) {
def main(args: Array[String]) {
try run(args)
catch {
case x: Exception =>
x.printStackTrace()
System.exit(1)
}
}
}

class ScalafmtProcessor extends Processor {
def processRequest(args: java.util.List[String]) {
val argName = List("config", "input", "output")
val argFile = args.asScala.map{x => new File(x)}
val namespace = argName.zip(argFile).toMap

val source = FileOps.readFile(namespace.getOrElse("input", new File("")))(Codec.UTF8)

val config = Config.fromHoconFile(namespace.getOrElse("config", new File(""))).get
@tailrec
def format(code: String): String = {
val formatted = Scalafmt.format(code, config).get
if (code == formatted) code else format(formatted)
}

val output = try {
format(source)
} catch {
case e @ (_: org.scalafmt.Error | _: scala.meta.parsers.ParseException) => {
System.out.println("Unable to format file due to bug in scalafmt")
System.out.println(e.toString)
source
}
}

Files.write(namespace.getOrElse("output", new File("")).toPath, output.getBytes)
}
}
Loading

0 comments on commit 0086b13

Please sign in to comment.