Skip to content

Commit 7df6c78

Browse files
authored
Warn when .java & .scala sources are used in a mixed compilation with disabled build server (VirtusLab#4181)
1 parent bc4f0b0 commit 7df6c78

5 files changed

Lines changed: 137 additions & 27 deletions

File tree

modules/build/src/main/scala/scala/build/Build.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,24 @@ object Build {
11991199
)
12001200
}
12011201

1202+
if sources.hasJava && sources.hasScala && options.useBuildServer.contains(false) then {
1203+
val javaPaths = sources.paths
1204+
.filter(_._1.last.endsWith(".java"))
1205+
.map(_._1.toString) ++
1206+
sources.inMemory
1207+
.filter(_.generatedRelPath.last.endsWith(".java"))
1208+
.map(_.originalPath.fold(identity, _._2.toString))
1209+
val javaPathsList =
1210+
javaPaths.map(p => s" $p").mkString(System.lineSeparator())
1211+
logger.message(
1212+
s"""$warnPrefix With ${Console.BOLD}--server=false${Console.RESET}, .java files are not compiled to .class files.
1213+
|scalac parses .java sources for type information (cross-compilation), but without the build server (Bloop/Zinc) nothing compiles them to bytecode.
1214+
|Affected .java files:
1215+
|$javaPathsList
1216+
|Remove --server=false or compile Java files separately to avoid runtime NoClassDefFoundError.""".stripMargin
1217+
)
1218+
}
1219+
12021220
buildClient.clear()
12031221
buildClient.setGeneratedSources(scope, generatedSources)
12041222

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
11
package scala.build.tests
22

3-
class BuildTestsScalac extends BuildTests(server = false)
3+
class BuildTestsScalac extends BuildTests(server = false) {
4+
5+
test("warn about Java files in mixed compilation with --server=false") {
6+
val recordingLogger = new RecordingLogger()
7+
val inputs = TestInputs(
8+
os.rel / "Side.java" ->
9+
"""public class Side {
10+
| public static String message = "Hello";
11+
|}
12+
|""".stripMargin,
13+
os.rel / "Main.scala" ->
14+
"""@main def main() = println(Side.message)
15+
|""".stripMargin
16+
)
17+
val options = defaultScala3Options.copy(useBuildServer = Some(false))
18+
inputs.withBuild(options, buildThreads, bloopConfigOpt, logger = Some(recordingLogger)) {
19+
(_, _, maybeBuild) =>
20+
assert(maybeBuild.isRight)
21+
val hasWarning = recordingLogger.messages.exists { msg =>
22+
msg.contains(".java files are not compiled to .class files") &&
23+
msg.contains("--server=false") &&
24+
msg.contains("Affected .java files")
25+
}
26+
assert(
27+
hasWarning,
28+
s"Expected warning about Java files with --server=false in: ${recordingLogger.messages.mkString("\n")}"
29+
)
30+
}
31+
}
32+
}

modules/build/src/test/scala/scala/build/tests/TestInputs.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import scala.build.compiler.{BloopCompilerMaker, SimpleScalaCompilerMaker}
88
import scala.build.errors.BuildException
99
import scala.build.input.{Inputs, ScalaCliInvokeData}
1010
import scala.build.options.{BuildOptions, Scope}
11-
import scala.build.{Build, BuildThreads, Builds}
11+
import scala.build.{Build, BuildThreads, Builds, Logger}
1212
import scala.util.Try
1313
import scala.util.control.NonFatal
1414

@@ -94,7 +94,8 @@ final case class TestInputs(
9494
fromDirectory: Boolean = false,
9595
buildTests: Boolean = true,
9696
actionableDiagnostics: Boolean = false,
97-
skipCreatingSources: Boolean = false
97+
skipCreatingSources: Boolean = false,
98+
logger: Option[Logger] = None
9899
)(f: (os.Path, Inputs, Either[BuildException, Builds]) => T): T =
99100
withCustomInputs(fromDirectory, None, skipCreatingSources) { (root, inputs) =>
100101
val compilerMaker = bloopConfigOpt match {
@@ -108,13 +109,14 @@ final case class TestInputs(
108109
case None =>
109110
SimpleScalaCompilerMaker("java", Nil)
110111
}
112+
val log = logger.getOrElse(TestLogger())
111113
val builds =
112114
Build.build(
113115
inputs,
114116
options,
115117
compilerMaker,
116118
None,
117-
TestLogger(),
119+
log,
118120
crossBuilds = false,
119121
buildTests = buildTests,
120122
partial = None,
@@ -131,7 +133,8 @@ final case class TestInputs(
131133
buildTests: Boolean = true,
132134
actionableDiagnostics: Boolean = false,
133135
scope: Scope = Scope.Main,
134-
skipCreatingSources: Boolean = false
136+
skipCreatingSources: Boolean = false,
137+
logger: Option[Logger] = None
135138
)(f: (os.Path, Inputs, Either[BuildException, Build]) => T): T =
136139
withBuilds(
137140
options,
@@ -140,7 +143,8 @@ final case class TestInputs(
140143
fromDirectory,
141144
buildTests = buildTests,
142145
actionableDiagnostics = actionableDiagnostics,
143-
skipCreatingSources = skipCreatingSources
146+
skipCreatingSources = skipCreatingSources,
147+
logger = logger
144148
) {
145149
(p, i, builds) =>
146150
f(

modules/build/src/test/scala/scala/build/tests/TestLogger.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,43 @@ import java.io.PrintStream
1010
import scala.build.Logger
1111
import scala.build.errors.{BuildException, Diagnostic}
1212
import scala.build.internals.FeatureType
13+
import scala.collection.mutable.ListBuffer
1314
import scala.scalanative.build as sn
1415

16+
/** Logger that records all message() and log() calls for test assertions. */
17+
final class RecordingLogger(delegate: Logger = TestLogger()) extends Logger {
18+
val messages: ListBuffer[String] = ListBuffer.empty
19+
20+
override def error(message: String): Unit = delegate.error(message)
21+
override def message(message: => String): Unit = {
22+
val msg = message
23+
messages += msg
24+
delegate.message(msg)
25+
}
26+
override def log(s: => String): Unit = {
27+
val msg = s
28+
messages += msg
29+
delegate.log(msg)
30+
}
31+
override def log(s: => String, debug: => String): Unit = delegate.log(s, debug)
32+
override def debug(s: => String): Unit = delegate.debug(s)
33+
override def log(diagnostics: Seq[Diagnostic]): Unit = delegate.log(diagnostics)
34+
override def log(ex: BuildException): Unit = delegate.log(ex)
35+
override def debug(ex: BuildException): Unit = delegate.debug(ex)
36+
override def exit(ex: BuildException): Nothing = delegate.exit(ex)
37+
override def coursierLogger(message: String): CacheLogger = delegate.coursierLogger(message)
38+
override def bloopRifleLogger: BloopRifleLogger = delegate.bloopRifleLogger
39+
override def scalaJsLogger: ScalaJsLogger = delegate.scalaJsLogger
40+
override def scalaNativeTestLogger: sn.Logger = delegate.scalaNativeTestLogger
41+
override def scalaNativeCliInternalLoggerOptions: List[String] =
42+
delegate.scalaNativeCliInternalLoggerOptions
43+
override def compilerOutputStream: PrintStream = delegate.compilerOutputStream
44+
override def verbosity: Int = delegate.verbosity
45+
override def experimentalWarning(featureName: String, featureType: FeatureType): Unit =
46+
delegate.experimentalWarning(featureName, featureType)
47+
override def flushExperimentalWarnings: Unit = delegate.flushExperimentalWarnings
48+
}
49+
1550
case class TestLogger(info: Boolean = true, debug: Boolean = false) extends Logger {
1651
override def log(diagnostics: Seq[Diagnostic]): Unit = {
1752
diagnostics.foreach { d =>

modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@ abstract class CompileTestDefinitions
6262
|""".stripMargin
6363
)
6464

65-
test(
66-
"java files with no using directives should not produce warnings about using directives in multiple files"
67-
) {
65+
{
6866
val inputs = TestInputs(
6967
os.rel / "Bar.java" ->
7068
"""public class Bar {}
@@ -73,12 +71,23 @@ abstract class CompileTestDefinitions
7371
"""public class Foo {}
7472
|""".stripMargin
7573
)
76-
77-
inputs.fromRoot { root =>
78-
val warningMessage = "Using directives detected in multiple files"
79-
val output = os.proc(TestUtil.cli, "compile", extraOptions, ".")
80-
.call(cwd = root, stderr = os.Pipe).err.trim()
81-
expect(!output.contains(warningMessage))
74+
test(
75+
"java files with no using directives should not produce warnings about using directives in multiple files"
76+
) {
77+
inputs.fromRoot { root =>
78+
val warningMessage = "Using directives detected in multiple files"
79+
val output = os.proc(TestUtil.cli, "compile", extraOptions, ".")
80+
.call(cwd = root, stderr = os.Pipe).err.trim()
81+
expect(!output.contains(warningMessage))
82+
}
83+
}
84+
test("Pure Java with --server=false: no warning about .java files not being compiled") {
85+
inputs.fromRoot { root =>
86+
val warningMessage = ".java files are not compiled to .class files"
87+
val output = os.proc(TestUtil.cli, "compile", "--server=false", extraOptions, ".")
88+
.call(cwd = root, stderr = os.Pipe).err.text()
89+
expect(!output.contains(warningMessage))
90+
}
8291
}
8392
}
8493

@@ -140,7 +149,7 @@ abstract class CompileTestDefinitions
140149
}
141150

142151
test(
143-
"having target + using directives in files should not produce warnings about using directives in multiple files"
152+
"having target + using directives in files: no using-directives or .java-not-compiled warnings"
144153
) {
145154
val inputs = TestInputs(
146155
os.rel / "Bar.java" ->
@@ -160,14 +169,14 @@ abstract class CompileTestDefinitions
160169
val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".")
161170
.call(cwd = root).err.trim()
162171
expect(!output.contains(warningMessage))
172+
expect(!output.contains(".java files are not compiled to .class files"))
163173
}
164174
}
165175

166-
test(
167-
"warn about directives in multiple files"
168-
) {
169-
val inputs = TestInputs(
170-
os.rel / "Bar.java" ->
176+
{
177+
val javaSourceFile = "Bar.java"
178+
val inputs = TestInputs(
179+
os.rel / javaSourceFile ->
171180
"""//> using jvm 17
172181
|public class Bar {}
173182
|""".stripMargin,
@@ -176,12 +185,24 @@ abstract class CompileTestDefinitions
176185
|class Foo {}
177186
|""".stripMargin
178187
)
188+
test("warn about directives in multiple files") {
189+
inputs.fromRoot { root =>
190+
val warningMessage = "Using directives detected in multiple files"
191+
val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".")
192+
.call(cwd = root, stderr = os.Pipe).err.trim()
193+
expect(output.contains(warningMessage))
194+
}
195+
}
179196

180-
inputs.fromRoot { root =>
181-
val warningMessage = "Using directives detected in multiple files"
182-
val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".")
183-
.call(cwd = root, stderr = os.Pipe).err.trim()
184-
expect(output.contains(warningMessage))
197+
test("mixed .java/.scala: with --server=false warn about .java not compiled") {
198+
inputs.fromRoot { root =>
199+
val warningMessage = ".java files are not compiled to .class files"
200+
val output =
201+
os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".", "--server=false")
202+
.call(cwd = root, stderr = os.Pipe).err.trim()
203+
expect(output.contains(warningMessage))
204+
expect(output.contains(javaSourceFile))
205+
}
185206
}
186207
}
187208

@@ -699,7 +720,9 @@ abstract class CompileTestDefinitions
699720
}
700721
}
701722

702-
test("pass java options to scalac when server=false") {
723+
test(
724+
"pass java options to scalac when server=false (Scala-only, no .java-not-compiled warning)"
725+
) {
703726
val inputs = TestInputs(
704727
os.rel / "Main.scala" ->
705728
"""object Main extends App {
@@ -721,6 +744,7 @@ abstract class CompileTestDefinitions
721744
val out = res.out.text()
722745
expect(out.contains("Error occurred during initialization of VM"))
723746
expect(out.contains("Too small maximum heap"))
747+
expect(!out.contains(".java files are not compiled to .class files"))
724748
}
725749
}
726750

0 commit comments

Comments
 (0)