From d9e5617e46a45ff4048cd9f07cf7f126afcd46e5 Mon Sep 17 00:00:00 2001 From: name_snrl <72071763+name-snrl@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:03:42 +0500 Subject: [PATCH] feat: add compiler diagnostics (#33) --- rules/common/private/get_toolchain.bzl | 2 + rules/private/phases/phase_zinc_compile.bzl | 6 +- scala/workers/zinc/compile/BUILD | 14 ++++ .../zinc/compile/main/ZincRunner.scala | 78 ++++++++++++++++++- .../zinc/compile/protobuf/diagnostics.proto | 51 ++++++++++++ scala3/private/toolchain_constants.bzl | 4 + scala3/repositories.bzl | 2 + scala3/toolchain.bzl | 1 + 8 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 scala/workers/zinc/compile/protobuf/diagnostics.proto diff --git a/rules/common/private/get_toolchain.bzl b/rules/common/private/get_toolchain.bzl index de4620b..c78a0cd 100644 --- a/rules/common/private/get_toolchain.bzl +++ b/rules/common/private/get_toolchain.bzl @@ -12,6 +12,7 @@ load( def _gen_toolchain(scala, toolchain): toolchain_info = platform_common.ToolchainInfo( scala_version = scala[_ScalaConfiguration].version, + enable_diagnostics = toolchain.enable_diagnostics, enable_semanticdb = toolchain.enable_semanticdb, semanticdb_bundle_in_jar = toolchain.semanticdb_bundle_in_jar, is_zinc = True if _ZincConfiguration in scala else False, @@ -46,6 +47,7 @@ def get_toolchain(ctx): if getattr(ctx.attr, "_worker_rule", False): stub_toolchain = platform_common.ToolchainInfo( + enable_diagnostics = False, enable_semanticdb = False, semanticdb_bundle_in_jar = False, ) diff --git a/rules/private/phases/phase_zinc_compile.bzl b/rules/private/phases/phase_zinc_compile.bzl index 487cb23..87fc551 100644 --- a/rules/private/phases/phase_zinc_compile.bzl +++ b/rules/private/phases/phase_zinc_compile.bzl @@ -89,6 +89,10 @@ def phase_zinc_compile(ctx, g): args.add_all(g.classpaths.src_jars, format_each = "--source_jar=%s") args.add("--tmp", tmp.path) args.add("--log_level", toolchain.zinc_log_level) + args.add("--enable_diagnostics", toolchain.enable_diagnostics) + if toolchain.enable_diagnostics: + diagnostics_file = ctx.actions.declare_file("{}.diagnosticsproto".format(ctx.label.name)) + args.add("--diagnostics_file", diagnostics_file) args.add_all(g.classpaths.srcs) args.set_param_file_format("multiline") args.use_param_file("@%s", use_always = True) @@ -106,7 +110,7 @@ def phase_zinc_compile(ctx, g): ] + [zinc.deps_files for zinc in zincs], ) - outputs = [g.classpaths.jar, mains_file, apis, infos, relations, setup, stamps, used, tmp] + semanticdb_files + outputs = [g.classpaths.jar, mains_file, apis, infos, relations, setup, stamps, used, tmp] + semanticdb_files + ([diagnostics_file] if toolchain.enable_diagnostics else []) # todo: different execution path for nosrc jar? ctx.actions.run( diff --git a/scala/workers/zinc/compile/BUILD b/scala/workers/zinc/compile/BUILD index 9d642bb..e83b105 100644 --- a/scala/workers/zinc/compile/BUILD +++ b/scala/workers/zinc/compile/BUILD @@ -1,6 +1,18 @@ +load("@rules_proto//proto:defs.bzl", "proto_library") load("//rules:scala.bzl", "scala_test", "scala_library") load("//scala3:defs.bzl", "worker_scala_binary") +proto_library( + name = "diagnostics_proto", + srcs = ["protobuf/diagnostics.proto"], +) + +java_proto_library( + name = "diagnostics_java_proto", + visibility = ["//visibility:public"], + deps = [":diagnostics_proto"], +) + worker_scala_binary( name = "compile", srcs = glob(["main/*.scala"]), @@ -20,6 +32,7 @@ worker_scala_binary( "@rules_scala3//rules/third_party/jarhelper", "@rules_scala3//scala/common/worker", "@rules_scala3//scala/workers/common", + ":diagnostics_java_proto", ], ) @@ -40,6 +53,7 @@ scala_library( "@rules_scala3//rules/third_party/jarhelper", "@rules_scala3//scala/common/worker", "@rules_scala3//scala/workers/common", + ":diagnostics_java_proto", ], scala = "@//:zinc_3", ) diff --git a/scala/workers/zinc/compile/main/ZincRunner.scala b/scala/workers/zinc/compile/main/ZincRunner.scala index 8f3af02..59c80b4 100644 --- a/scala/workers/zinc/compile/main/ZincRunner.scala +++ b/scala/workers/zinc/compile/main/ZincRunner.scala @@ -9,6 +9,7 @@ import java.text.SimpleDateFormat import java.util.{Date, List as JList, Optional, Properties} import scala.jdk.CollectionConverters.* +import scala.jdk.OptionConverters.RichOptional import scala.util.Try import scala.util.control.NonFatal @@ -16,7 +17,7 @@ import com.google.devtools.build.buildjar.jarhelper.JarCreator import sbt.internal.inc.{Analysis, AnalyzingCompiler, CompileFailed, IncrementalCompilerImpl, Locate, PlainVirtualFile, ZincUtil} import sbt.internal.inc.classpath.ClassLoaderCache import scopt.{DefaultOParserSetup, OParser, OParserSetup} -import xsbti.{Logger, PathBasedFile, VirtualFile} +import xsbti.{Logger, PathBasedFile, Problem, Severity, VirtualFile} import xsbti.compile.{ AnalysisContents, ClasspathOptionsUtil, @@ -35,6 +36,41 @@ import xsbti.compile.{ import rules_scala.common.worker.WorkerMain import rules_scala.workers.common.* +import rules_scala.workers.zinc.diagnostics.Diagnostics; + +extension (problem: Problem) + def toDiagnostic: Diagnostics.Diagnostic = + def e = + val path = problem.position.sourcePath.toScala.fold("no data")(identity) + val line = problem.position.line.toScala.fold("no data")(identity) + val pointer = problem.position.pointer.toScala.fold("no data")(identity) + sys.error(s"The compilation Problem($path:$line:$pointer) does not contain enough information. Failed to create compilation diagnostics.") + + Diagnostics.Diagnostic.newBuilder + .setSeverity { + problem.severity match + case Severity.Error => Diagnostics.Severity.ERROR + case Severity.Warn => Diagnostics.Severity.WARNING + case Severity.Info => Diagnostics.Severity.INFORMATION + } + .setMessage(problem.message) + .setRange { + Diagnostics.Range.newBuilder + .setStart( + Diagnostics.Position.newBuilder + .setLine(problem.position.startLine().toScala.fold(e)(identity) - 1) + .setCharacter(problem.position.startColumn().toScala.fold(e)(identity)) + .build + ) + .setEnd( + Diagnostics.Position.newBuilder + .setLine(problem.position.endLine().toScala.fold(e)(identity) - 1) + .setCharacter(problem.position.endColumn().toScala.fold(e)(identity)) + .build + ) + .build + } + .build final case class AnalysisArgument(label: String, apis: Path, relations: Path, jars: Vector[Path]) object AnalysisArgument: @@ -72,6 +108,8 @@ object ZincRunner extends WorkerMain[ZincRunner.Arguments]: given logger: AnnexLogger = AnnexLogger(workArgs.logLevel) + val reporter = LoggedReporter(logger) + val sourcesDir = workArgs.tmpDir.resolve("src") val sources: collection.Seq[File] = workArgs.sources ++ workArgs.sourceJars.zipWithIndex .flatMap((jar, i) => FileUtil.extractZip(jar, sourcesDir.resolve(i.toString))) @@ -167,7 +205,6 @@ object ZincRunner extends WorkerMain[ZincRunner.Arguments]: val setup = val incOptions = IncOptions.create() - val reporter = LoggedReporter(logger) val skip = false val zincFile: Path = null @@ -201,6 +238,27 @@ object ZincRunner extends WorkerMain[ZincRunner.Arguments]: System.err.println(s"workArgs:$workArgs") System.err.println(e) sys.exit(1) + finally + // create compiler diagnostics + if workArgs.enableDiagnostics then + val targetDiagnostics: Diagnostics.TargetDiagnostics = + Diagnostics.TargetDiagnostics.newBuilder.addAllDiagnostics { + reporter.problems + .groupBy( + _.position.sourcePath.toScala + .fold(sys.error("The compilation problem does not contain the path to the source. Failed to create compilation diagnostics."))( + identity + ) + ) + .map { case (path, problems) => + Diagnostics.FileDiagnostics.newBuilder + .setPath("workspace-root://" + path) + .addAllDiagnostics(problems.map(_.toDiagnostic).toList.asJava) + .build + } + .asJava + }.build + Files.write(workArgs.diagnosticsFile, targetDiagnostics.toByteArray) // create analyses analysisStore.set(AnalysisContents.create(compileResult.analysis, compileResult.setup)) @@ -272,6 +330,8 @@ object ZincRunner extends WorkerMain[ZincRunner.Arguments]: javaCompilerOption: Vector[String] = Vector.empty, label: String = "", logLevel: LogLevel = LogLevel.Debug, + enableDiagnostics: Boolean = false, + diagnosticsFile: Path = Paths.get("."), mainManifest: File = new File("."), outputApis: Path = Paths.get("."), outputInfos: Path = Paths.get("."), @@ -379,7 +439,19 @@ object ZincRunner extends WorkerMain[ZincRunner.Arguments]: .unbounded() .optional() .action((s, c) => c.copy(sources = c.sources :+ s)) - .text("Source files") + .text("Source files"), + opt[Boolean]("enable_diagnostics").action((value, c) => c.copy(enableDiagnostics = value)), + opt[File]("diagnostics_file") + .optional() + .action((value, c) => c.copy(diagnosticsFile = value.toPath)) + .text("Compilation diagnostics file"), + + checkConfig { c => + if c.enableDiagnostics && c.diagnosticsFile == Paths.get(".") then + failure("If enable_diagnostics is true, diagnostics_file must be specified") + else + success + } ) def apply(args: collection.Seq[String]): Option[WorkArguments] = diff --git a/scala/workers/zinc/compile/protobuf/diagnostics.proto b/scala/workers/zinc/compile/protobuf/diagnostics.proto new file mode 100644 index 0000000..263016e --- /dev/null +++ b/scala/workers/zinc/compile/protobuf/diagnostics.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +option java_package = "rules_scala.workers.zinc.diagnostics"; +option java_outer_classname = "Diagnostics"; + +enum Severity { + UNKNOWN = 0; + ERROR = 1; + WARNING = 2; + INFORMATION = 3; + HINT = 4; +} + +message Position { + // 1-indexed + int32 line = 1; + // 1-indexed + int32 character = 2; +} + +message Range { + Position start = 1; + Position end = 2; +} + +message Location { + string path = 1; + Range range = 2; +} + +message DiagnosticRelatedInformation { + Location location = 1; + string message = 2; +} + +message Diagnostic { + Range range = 1; + Severity severity = 2; + int64 code = 3; + string message = 5; + repeated DiagnosticRelatedInformation related_information = 6; +} + +message FileDiagnostics { + string path = 1; + repeated Diagnostic diagnostics = 2; +} + +message TargetDiagnostics { + repeated FileDiagnostics diagnostics = 1; +} diff --git a/scala3/private/toolchain_constants.bzl b/scala3/private/toolchain_constants.bzl index 0086c83..d5b9cc7 100644 --- a/scala3/private/toolchain_constants.bzl +++ b/scala3/private/toolchain_constants.bzl @@ -1,6 +1,10 @@ "A module containing some constants related to the toolchain" SCALA_TOOLCHAIN_ATTRS = { + "enable_diagnostics": attr.bool( + default = False, + doc = "Enable compiler diagnostic report.", + ), "enable_semanticdb": attr.bool( default = False, doc = "Enable SemanticDB.", diff --git a/scala3/repositories.bzl b/scala3/repositories.bzl index e4493a1..a151046 100644 --- a/scala3/repositories.bzl +++ b/scala3/repositories.bzl @@ -80,6 +80,7 @@ def scala3_register_toolchains( { "name": "scala3_mezel_toolchain", "enable_semanticdb": True, + "enable_diagnostics": True, "global_scalacopts": ["-Xfatal-warnings"], }, ] @@ -148,6 +149,7 @@ config_setting(name = "deps_used_error", flag_values = {{ ":deps_used": "error" scala_toolchain( name = "toolchain_impl", scala_version = "{scala_version}", + enable_diagnostics = {enable_diagnostics}, enable_semanticdb = {enable_semanticdb}, semanticdb_bundle_in_jar = {semanticdb_bundle_in_jar}, is_zinc = {is_zinc}, diff --git a/scala3/toolchain.bzl b/scala3/toolchain.bzl index 7262ae1..abda773 100644 --- a/scala3/toolchain.bzl +++ b/scala3/toolchain.bzl @@ -30,6 +30,7 @@ def _scala_toolchain_impl(ctx): toolchain_info = platform_common.ToolchainInfo( scala_version = ctx.attr.scala_version, + enable_diagnostics = ctx.attr.enable_diagnostics, enable_semanticdb = ctx.attr.enable_semanticdb, semanticdb_bundle_in_jar = ctx.attr.semanticdb_bundle_in_jar, is_zinc = ctx.attr.is_zinc,