Skip to content

Commit 6ba125f

Browse files
committed
plugin2
1 parent 7657d2f commit 6ba125f

File tree

8 files changed

+406
-2
lines changed

8 files changed

+406
-2
lines changed

scala/private/phases/phase_compile.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def phase_compile_library_for_plugin_bootstrapping(ctx, p):
5959
for target in p.scalac_provider.default_classpath + ctx.attr.exports
6060
],
6161
unused_dependency_checker_mode = "off",
62+
buildijar = ctx.attr.build_ijar,
6263
)
6364
return _phase_compile_default(ctx, p, args)
6465

scala/private/rules/scala_library.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ _scala_library_for_plugin_bootstrapping_attrs.update(
161161
common_attrs_for_plugin_bootstrapping,
162162
)
163163

164+
_scala_library_for_plugin_bootstrapping_attrs["build_ijar"] = attr.bool(default = True)
165+
164166
def make_scala_library_for_plugin_bootstrapping(*extras):
165167
return rule(
166168
attrs = _dicts.add(

third_party/dependency_analyzer/src/main/BUILD

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ licenses(["notice"]) # 3-clause BSD
22

33
load("//scala:scala.bzl", "scala_library_for_plugin_bootstrapping")
44

5+
scala_library_for_plugin_bootstrapping(
6+
name = "scala_version",
7+
srcs = [
8+
"io/bazel/rulesscala/dependencyanalyzer/ScalaVersion.scala",
9+
],
10+
# As this contains macros we shouldn't make an ijar
11+
build_ijar = False,
12+
resources = ["resources/scalac-plugin.xml"],
13+
visibility = ["//visibility:public"],
14+
deps = [
15+
"//external:io_bazel_rules_scala/dependency/scala/scala_compiler",
16+
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
17+
],
18+
)
19+
520
scala_library_for_plugin_bootstrapping(
621
name = "dependency_analyzer",
722
srcs = [
@@ -14,6 +29,7 @@ scala_library_for_plugin_bootstrapping(
1429
resources = ["resources/scalac-plugin.xml"],
1530
visibility = ["//visibility:public"],
1631
deps = [
32+
":scala_version",
1733
"//external:io_bazel_rules_scala/dependency/scala/scala_compiler",
1834
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
1935
],

third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/AstUsedJarFinder.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ class AstUsedJarFinder(
3737
node.original.foreach(fullyExploreTree)
3838
}
3939
case node: Literal =>
40+
// We should examine OriginalTreeAttachment but that was only
41+
// added in 2.12.4, so include a version check
42+
ScalaVersion.conditional(
43+
"2.12.4",
44+
"",
45+
"""
46+
node.attachments
47+
.get[global.treeChecker.OriginalTreeAttachment]
48+
.foreach { attach =>
49+
fullyExploreTree(attach.original)
50+
}
51+
"""
52+
)
53+
4054
node.value.value match {
4155
case tpe: Type =>
4256
exploreType(tpe)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer
2+
3+
import scala.language.experimental.macros
4+
import scala.reflect.macros.blackbox
5+
6+
object ScalaVersion {
7+
val Current: ScalaVersion = ScalaVersion(util.Properties.versionNumberString)
8+
9+
def apply(versionString: String): ScalaVersion = {
10+
versionString.split("\\.") match {
11+
case Array(superMajor, major, minor) =>
12+
new ScalaVersion(superMajor.toInt, major.toInt, minor.toInt)
13+
case _ =>
14+
throw new Exception(s"Failed to parse version $versionString")
15+
}
16+
}
17+
18+
/**
19+
* Runs [code] only if minVersion and maxVersion constraints are met.
20+
*
21+
* NOTE: This method should be used only rarely. Most of the time
22+
* just comparing versions in code should be enough. This is needed
23+
* only when the code we want to run can't compile under certain
24+
* versions. The reason to use this rarely is the API's inflexibility
25+
* and the difficulty in debugging this code.
26+
*
27+
* Each of minVersion and maxVersion can either be the empty string ("")
28+
* to signify that there is no restriction on this bound.
29+
*
30+
* Or it can be a string of a full version number such as "2.12.10".
31+
*
32+
* Note only literal strings are accepted, no variables etc. i.e.
33+
*
34+
* valid:
35+
* conditional(minVersion = "2.12.4", maxVersion = "", code = "foo()")
36+
* invalid:
37+
* conditional(minVersion = MinVersionForFoo, maxVersion = "", code = "foo()")
38+
*/
39+
def conditional(
40+
minVersion: String,
41+
maxVersion: String,
42+
code: String
43+
): Unit =
44+
macro conditionalImpl
45+
46+
def conditionalImpl(
47+
c: blackbox.Context
48+
)(
49+
minVersion: c.Expr[String],
50+
maxVersion: c.Expr[String],
51+
code: c.Expr[String]
52+
): c.Tree = {
53+
import c.{universe => u}
54+
import u.Quasiquote
55+
def extractString(expr: c.Expr[String]): String = {
56+
expr.tree match {
57+
case u.Literal(u.Constant(s: String)) =>
58+
s
59+
case _ =>
60+
c.error(
61+
expr.tree.pos,
62+
"Parameter must be passed as a string literal such as \"2.12.10\"")
63+
""
64+
}
65+
}
66+
67+
val meetsMinVersionRequirement = {
68+
val minVersionStr = extractString(minVersion)
69+
minVersionStr == "" || Current >= ScalaVersion(minVersionStr)
70+
}
71+
72+
val meetsMaxVersionRequirement = {
73+
val maxVersionStr = extractString(maxVersion)
74+
maxVersionStr == "" || Current <= ScalaVersion(maxVersionStr)
75+
}
76+
77+
if (meetsMinVersionRequirement && meetsMaxVersionRequirement) {
78+
c.parse(extractString(code))
79+
} else {
80+
q""
81+
}
82+
}
83+
}
84+
85+
class ScalaVersion private(
86+
private val superMajor: Int,
87+
private val major: Int,
88+
private val minor: Int
89+
) extends Ordered[ScalaVersion] {
90+
override def compare(that: ScalaVersion): Int = {
91+
if (this.superMajor != that.superMajor) {
92+
this.superMajor.compareTo(that.superMajor)
93+
} else if (this.major != that.major) {
94+
this.major.compareTo(that.major)
95+
} else {
96+
this.minor.compareTo(that.minor)
97+
}
98+
}
99+
100+
override def equals(obj: Any): Boolean = {
101+
obj match {
102+
case that: ScalaVersion =>
103+
compare(that) == 0
104+
case _ =>
105+
false
106+
}
107+
}
108+
109+
override def toString: String = {
110+
s"$superMajor.$major.$minor"
111+
}
112+
}

third_party/dependency_analyzer/src/test/BUILD

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,29 @@ scala_test(
1414
"io/bazel/rulesscala/dependencyanalyzer/AstUsedJarFinderTest.scala",
1515
],
1616
jvm_flags = common_jvm_flags,
17-
unused_dependency_checker_mode = "off",
1817
deps = [
19-
"//external:io_bazel_rules_scala/dependency/scala/scala_compiler",
2018
"//external:io_bazel_rules_scala/dependency/scala/scala_library",
2119
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
2220
"//third_party/dependency_analyzer/src/main:dependency_analyzer",
21+
"//third_party/dependency_analyzer/src/main:scala_version",
2322
"//third_party/utils/src/test:test_util",
2423
"@scalac_rules_commons_io//jar",
2524
],
2625
)
2726

27+
scala_test(
28+
name = "scala_version_test",
29+
size = "small",
30+
srcs = [
31+
"io/bazel/rulesscala/dependencyanalyzer/ScalaVersionTest.scala",
32+
],
33+
deps = [
34+
"//external:io_bazel_rules_scala/dependency/scala/scala_library",
35+
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
36+
"//third_party/dependency_analyzer/src/main:scala_version",
37+
],
38+
)
39+
2840
scala_test(
2941
name = "strict_deps_test",
3042
size = "small",

third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/AstUsedJarFinderTest.scala

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import java.nio.file.Path
55
import org.apache.commons.io.FileUtils
66
import org.scalatest._
77
import third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer.DependencyTrackingMethod
8+
import third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer.ScalaVersion
89
import third_party.utils.src.test.io.bazel.rulesscala.utils.JavaCompileUtil
910
import third_party.utils.src.test.io.bazel.rulesscala.utils.TestUtil
1011
import third_party.utils.src.test.io.bazel.rulesscala.utils.TestUtil.DependencyAnalyzerTestParams
@@ -152,6 +153,23 @@ class AstUsedJarFinderTest extends FunSuite {
152153
}
153154
}
154155

156+
/**
157+
* In a situation where B depends indirectly on A, ensure
158+
* that the dependency analyzer recognizes this fact.
159+
*/
160+
private def checkIndirectDependencyDetected(
161+
aCode: String,
162+
bCode: String
163+
): Unit = {
164+
withSandbox { sandbox =>
165+
sandbox.compileWithoutAnalyzer(aCode)
166+
sandbox.checkUnusedDepsErrorReported(
167+
code = bCode,
168+
expectedUnusedDeps = List("A")
169+
)
170+
}
171+
}
172+
155173
test("simple composition in indirect") {
156174
checkIndirectDependencyDetected(
157175
aCode =
@@ -302,6 +320,43 @@ class AstUsedJarFinderTest extends FunSuite {
302320
)
303321
}
304322

323+
test("inlined literal is direct") {
324+
// Note: For a constant to be inlined
325+
// - it must not have a type declaration such as `: Int`.
326+
// (this appears to be the case in practice at least)
327+
// (is this documented anywhere???)
328+
// - some claim it must start with a capital letter, though
329+
// this does not seem to be the case. Nevertheless we do that
330+
// anyways.
331+
//
332+
// Hence it is possible that as newer versions of scala
333+
// are released then this test may need to be updated to
334+
// conform to changing requirements of what is inlined.
335+
336+
// Note that in versions of scala < 2.12.4 we cannot detect
337+
// such a situation. Hence we will have a false positive here
338+
// for those older versions, which we verify in test.
339+
340+
val aCode =
341+
s"""
342+
|object A {
343+
| final val Inlined = 123
344+
|}
345+
|""".stripMargin
346+
val bCode =
347+
s"""
348+
|object B {
349+
| val d: Int = A.Inlined
350+
|}
351+
|""".stripMargin
352+
353+
if (ScalaVersion.Current >= ScalaVersion("2.12.4")) {
354+
checkDirectDependencyRecognized(aCode = aCode, bCode = bCode)
355+
} else {
356+
checkIndirectDependencyDetected(aCode = aCode, bCode = bCode)
357+
}
358+
}
359+
305360
test("java interface method argument is direct") {
306361
withSandbox { sandbox =>
307362
sandbox.compileJava(
@@ -318,4 +373,60 @@ class AstUsedJarFinderTest extends FunSuite {
318373
)
319374
}
320375
}
376+
377+
test("java interface field and method is direct") {
378+
withSandbox { sandbox =>
379+
sandbox.compileJava(
380+
className = "A",
381+
code = "public interface A { int a = 42; }"
382+
)
383+
val bCode =
384+
"""
385+
|class B {
386+
| def foo(x: A): Unit = {}
387+
| val b = A.a
388+
|}
389+
|""".stripMargin
390+
391+
// It is unclear why this only works with these versions but
392+
// presumably there were various compiler improvements.
393+
if (ScalaVersion.Current >= ScalaVersion("2.12.0")) {
394+
sandbox.checkStrictDepsErrorsReported(
395+
bCode,
396+
expectedStrictDeps = List("A")
397+
)
398+
} else {
399+
sandbox.checkUnusedDepsErrorReported(
400+
bCode,
401+
expectedUnusedDeps = List("A")
402+
)
403+
}
404+
}
405+
}
406+
407+
test("java interface field is direct") {
408+
withSandbox { sandbox =>
409+
sandbox.compileJava(
410+
className = "A",
411+
code = "public interface A { int a = 42; }"
412+
)
413+
val bCode =
414+
"""
415+
|class B {
416+
| val b = A.a
417+
|}
418+
|""".stripMargin
419+
if (ScalaVersion.Current >= ScalaVersion("2.12.4")) {
420+
sandbox.checkStrictDepsErrorsReported(
421+
bCode,
422+
expectedStrictDeps = List("A")
423+
)
424+
} else {
425+
sandbox.checkUnusedDepsErrorReported(
426+
bCode,
427+
expectedUnusedDeps = List("A")
428+
)
429+
}
430+
}
431+
}
321432
}

0 commit comments

Comments
 (0)