Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phase Scalafmt #912

Merged
merged 38 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
54200b6
Phase Scalafmt
borkaehw Jan 7, 2020
930287e
Reuse formatter
borkaehw Jan 15, 2020
17dc7f3
Remove glob
borkaehw Jan 15, 2020
1ecd77c
Remove rules_jvm_external
borkaehw Jan 20, 2020
079c387
Rename argparse
borkaehw Jan 20, 2020
dcf7922
Remove executable
borkaehw Jan 20, 2020
1c9f3de
Use shared code
borkaehw Jan 20, 2020
4448f56
Remove imports
borkaehw Jan 21, 2020
5664976
Add comment
borkaehw Jan 21, 2020
8ce301c
Change file name
borkaehw Jan 21, 2020
26fcdca
Move args to private function
borkaehw Jan 21, 2020
f71478f
Change to true
borkaehw Jan 21, 2020
7845a78
Change conf location
borkaehw Jan 21, 2020
93de448
Change default conf
borkaehw Jan 21, 2020
6fb863d
Test custom conf
borkaehw Jan 21, 2020
82a5097
Fix lint
borkaehw Jan 21, 2020
444629d
Fix build
borkaehw Jan 22, 2020
da78069
Remove trailing commas
borkaehw Jan 22, 2020
7a4d7ff
Add formatted and unformatted folder
borkaehw Jan 22, 2020
3b7507c
Rename test function
borkaehw Jan 22, 2020
4d529b0
Remove template file
borkaehw Jan 22, 2020
b90c10e
Better handle failing case
borkaehw Jan 22, 2020
c935d83
Drop argparser
borkaehw Jan 22, 2020
0353a7d
Remove resolve_command
borkaehw Jan 22, 2020
05d70f5
Add comments
borkaehw Jan 22, 2020
1334d7a
Remove unnecessary code
borkaehw Jan 22, 2020
7585fbf
Change to RUNPATH
borkaehw Jan 23, 2020
19fb8b9
Rename gitignore backup
borkaehw Jan 23, 2020
9c387a2
Remove comment
borkaehw Jan 23, 2020
7988a24
Move conf file
borkaehw Jan 23, 2020
feeb970
Add doc to attribute
borkaehw Jan 27, 2020
10ee6d6
Switch to match readme
borkaehw Jan 27, 2020
36f4e3c
Add doc
borkaehw Jan 27, 2020
e856593
Move doc to separate md
borkaehw Jan 27, 2020
162e987
Fix wording
borkaehw Jan 27, 2020
271d7ff
Fix lint
borkaehw Jan 27, 2020
ec9f5ea
Add url
borkaehw Jan 28, 2020
a417b8b
Add url
borkaehw Jan 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
ittaiz marked this conversation as resolved.
Show resolved Hide resolved
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",
borkaehw marked this conversation as resolved.
Show resolved Hide resolved
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"],
ittaiz marked this conversation as resolved.
Show resolved Hide resolved
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(
ittaiz marked this conversation as resolved.
Show resolved Hide resolved
name = "phase_scalafmt",
ittaiz marked this conversation as resolved.
Show resolved Hide resolved
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(
ittaiz marked this conversation as resolved.
Show resolved Hide resolved
"//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