Skip to content

Commit dd9ce77

Browse files
authored
Handle inlined literals in AST walker (#985)
* Handle inlined literals in AST walker * comments * comments
1 parent ce97b82 commit dd9ce77

File tree

8 files changed

+473
-3
lines changed

8 files changed

+473
-3
lines changed

scala/private/phases/phase_compile.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def phase_compile_library_for_plugin_bootstrapping(ctx, p):
4747
for target in p.scalac_provider.default_classpath + ctx.attr.exports
4848
],
4949
unused_dependency_checker_mode = "off",
50+
buildijar = ctx.attr.build_ijar,
5051
)
5152
return _phase_compile_default(ctx, p, args)
5253

scala/private/rules/scala_library.bzl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ def _scala_library_for_plugin_bootstrapping_impl(ctx):
152152
# the scala compiler plugin used for dependency analysis is compiled using `scala_library`.
153153
# in order to avoid cyclic dependencies `scala_library_for_plugin_bootstrapping` was created for this purpose,
154154
# which does not contain plugin related attributes, and thus avoids the cyclic dependency issue
155-
_scala_library_for_plugin_bootstrapping_attrs = {}
155+
_scala_library_for_plugin_bootstrapping_attrs = {
156+
"build_ijar": attr.bool(default = True),
157+
}
156158

157159
_scala_library_for_plugin_bootstrapping_attrs.update(implicit_deps)
158160

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+
Some("2.12.4"),
44+
None,
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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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 minVersionOpt and maxVersionOpt can either be None
28+
* to signify that there is no restriction on this bound,
29+
* or it can be a string of a full version number such as "2.12.10".
30+
*
31+
* When set to a version number, the bounds are inclusive.
32+
* For example, a maxVersion of "2.12.10" will accept version "2.12.10".
33+
*
34+
* Note only literal strings are accepted and inlined variables are accepted.
35+
* If any non-inlined variables are passed the code will fail to compile.
36+
* Inlined variables are generally those declared final on an object which
37+
* do not have a type attached.
38+
*
39+
* valid:
40+
* conditional(Some("2.12.4"), None, "foo()")
41+
* invalid:
42+
* conditional(MinVersionForFoo, None, "foo()")
43+
*/
44+
def conditional(
45+
minVersionOpt: Option[String],
46+
maxVersionOpt: Option[String],
47+
code: String
48+
): Unit =
49+
macro conditionalImpl
50+
51+
def conditionalImpl(
52+
c: blackbox.Context
53+
)(
54+
minVersionOpt: c.Expr[Option[String]],
55+
maxVersionOpt: c.Expr[Option[String]],
56+
code: c.Expr[String]
57+
): c.Tree = {
58+
import c.{universe => u}
59+
60+
// Due to non-deterministic code generation of quasiquotes, we do
61+
// not use them
62+
// See https://github.com/scala/bug/issues/11008
63+
// Eventually once we stop supporting all versions which don't have
64+
// the bugfix, we can use quasiquotes as desired
65+
66+
def extractStringFromTree(tree: c.Tree): Option[String] = {
67+
tree match {
68+
case u.Literal(u.Constant(s: String)) =>
69+
Some(s)
70+
case _ =>
71+
None
72+
}
73+
}
74+
75+
def extractStringOption(expr: c.Expr[Option[String]]): Option[String] = {
76+
expr.tree match {
77+
case u.Apply(
78+
u.TypeApply(
79+
u.Select(u.Select(u.Ident(u.TermName("scala")), u.TermName("Some")), u.TermName("apply")),
80+
List(u.TypeTree())),
81+
str :: Nil
82+
) if extractStringFromTree(str).nonEmpty =>
83+
extractStringFromTree(str)
84+
case u.Select(u.Ident(u.TermName("scala")), u.TermName("None")) =>
85+
None
86+
case _ =>
87+
c.error(
88+
expr.tree.pos,
89+
"Parameter must be passed as an Option[String] literal such as " +
90+
"Some(\"2.12.10\") or None")
91+
None
92+
}
93+
}
94+
95+
def extractString(expr: c.Expr[String]): String = {
96+
extractStringFromTree(expr.tree).getOrElse {
97+
c.error(
98+
expr.tree.pos,
99+
"Parameter must be passed as a string literal such as \"2.12.10\"")
100+
""
101+
}
102+
}
103+
104+
val meetsMinVersionRequirement = {
105+
val minVersionOptValue = extractStringOption(minVersionOpt)
106+
107+
// Note: Unit tests do not test that this bound is inclusive rather
108+
// than exclusive so be careful when changing this code not to
109+
// accidentally make this an exclusive bound (see ScalaVersionTest for
110+
// details)
111+
minVersionOptValue.forall(version => Current >= ScalaVersion(version))
112+
}
113+
114+
val meetsMaxVersionRequirement = {
115+
val maxVersionOptValue = extractStringOption(maxVersionOpt)
116+
// Note: Unit tests do not test that this bound is inclusive rather
117+
// than exclusive so be careful when changing this code not to
118+
// accidentally make this an exclusive bound (see ScalaVersionTest for
119+
// details)
120+
maxVersionOptValue.forall(version => Current <= ScalaVersion(version))
121+
}
122+
123+
if (meetsMinVersionRequirement && meetsMaxVersionRequirement) {
124+
c.parse(extractString(code))
125+
} else {
126+
u.EmptyTree
127+
}
128+
}
129+
}
130+
131+
class ScalaVersion private(
132+
private val superMajor: Int,
133+
private val major: Int,
134+
private val minor: Int
135+
) extends Ordered[ScalaVersion] {
136+
override def compare(that: ScalaVersion): Int = {
137+
if (this.superMajor != that.superMajor) {
138+
this.superMajor.compareTo(that.superMajor)
139+
} else if (this.major != that.major) {
140+
this.major.compareTo(that.major)
141+
} else {
142+
this.minor.compareTo(that.minor)
143+
}
144+
}
145+
146+
override def equals(obj: Any): Boolean = {
147+
obj match {
148+
case that: ScalaVersion =>
149+
compare(that) == 0
150+
case _ =>
151+
false
152+
}
153+
}
154+
155+
override def toString: String = {
156+
s"$superMajor.$major.$minor"
157+
}
158+
}

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",

0 commit comments

Comments
 (0)