Skip to content

Commit

Permalink
Handle inlined literals in AST walker (bazelbuild#985)
Browse files Browse the repository at this point in the history
* Handle inlined literals in AST walker

* comments

* comments
  • Loading branch information
Jamie5 authored and Andre Rocha committed Jul 6, 2020
1 parent d6457c8 commit d534984
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 3 deletions.
1 change: 1 addition & 0 deletions scala/private/phases/phase_compile.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def phase_compile_library_for_plugin_bootstrapping(ctx, p):
for target in p.scalac_provider.default_classpath + ctx.attr.exports
],
unused_dependency_checker_mode = "off",
buildijar = ctx.attr.build_ijar,
)
return _phase_compile_default(ctx, p, args)

Expand Down
4 changes: 3 additions & 1 deletion scala/private/rules/scala_library.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ def _scala_library_for_plugin_bootstrapping_impl(ctx):
# the scala compiler plugin used for dependency analysis is compiled using `scala_library`.
# in order to avoid cyclic dependencies `scala_library_for_plugin_bootstrapping` was created for this purpose,
# which does not contain plugin related attributes, and thus avoids the cyclic dependency issue
_scala_library_for_plugin_bootstrapping_attrs = {}
_scala_library_for_plugin_bootstrapping_attrs = {
"build_ijar": attr.bool(default = True),
}

_scala_library_for_plugin_bootstrapping_attrs.update(implicit_deps)

Expand Down
16 changes: 16 additions & 0 deletions third_party/dependency_analyzer/src/main/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ licenses(["notice"]) # 3-clause BSD

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

scala_library_for_plugin_bootstrapping(
name = "scala_version",
srcs = [
"io/bazel/rulesscala/dependencyanalyzer/ScalaVersion.scala",
],
# As this contains macros we shouldn't make an ijar
build_ijar = False,
resources = ["resources/scalac-plugin.xml"],
visibility = ["//visibility:public"],
deps = [
"//external:io_bazel_rules_scala/dependency/scala/scala_compiler",
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
],
)

scala_library_for_plugin_bootstrapping(
name = "dependency_analyzer",
srcs = [
Expand All @@ -14,6 +29,7 @@ scala_library_for_plugin_bootstrapping(
resources = ["resources/scalac-plugin.xml"],
visibility = ["//visibility:public"],
deps = [
":scala_version",
"//external:io_bazel_rules_scala/dependency/scala/scala_compiler",
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ class AstUsedJarFinder(
node.original.foreach(fullyExploreTree)
}
case node: Literal =>
// We should examine OriginalTreeAttachment but that was only
// added in 2.12.4, so include a version check
ScalaVersion.conditional(
Some("2.12.4"),
None,
"""
node.attachments
.get[global.treeChecker.OriginalTreeAttachment]
.foreach { attach =>
fullyExploreTree(attach.original)
}
"""
)

node.value.value match {
case tpe: Type =>
exploreType(tpe)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

object ScalaVersion {
val Current: ScalaVersion = ScalaVersion(util.Properties.versionNumberString)

def apply(versionString: String): ScalaVersion = {
versionString.split("\\.") match {
case Array(superMajor, major, minor) =>
new ScalaVersion(superMajor.toInt, major.toInt, minor.toInt)
case _ =>
throw new Exception(s"Failed to parse version $versionString")
}
}

/**
* Runs [code] only if minVersion and maxVersion constraints are met.
*
* NOTE: This method should be used only rarely. Most of the time
* just comparing versions in code should be enough. This is needed
* only when the code we want to run can't compile under certain
* versions. The reason to use this rarely is the API's inflexibility
* and the difficulty in debugging this code.
*
* Each of minVersionOpt and maxVersionOpt can either be None
* to signify that there is no restriction on this bound,
* or it can be a string of a full version number such as "2.12.10".
*
* When set to a version number, the bounds are inclusive.
* For example, a maxVersion of "2.12.10" will accept version "2.12.10".
*
* Note only literal strings are accepted and inlined variables are accepted.
* If any non-inlined variables are passed the code will fail to compile.
* Inlined variables are generally those declared final on an object which
* do not have a type attached.
*
* valid:
* conditional(Some("2.12.4"), None, "foo()")
* invalid:
* conditional(MinVersionForFoo, None, "foo()")
*/
def conditional(
minVersionOpt: Option[String],
maxVersionOpt: Option[String],
code: String
): Unit =
macro conditionalImpl

def conditionalImpl(
c: blackbox.Context
)(
minVersionOpt: c.Expr[Option[String]],
maxVersionOpt: c.Expr[Option[String]],
code: c.Expr[String]
): c.Tree = {
import c.{universe => u}

// Due to non-deterministic code generation of quasiquotes, we do
// not use them
// See https://github.com/scala/bug/issues/11008
// Eventually once we stop supporting all versions which don't have
// the bugfix, we can use quasiquotes as desired

def extractStringFromTree(tree: c.Tree): Option[String] = {
tree match {
case u.Literal(u.Constant(s: String)) =>
Some(s)
case _ =>
None
}
}

def extractStringOption(expr: c.Expr[Option[String]]): Option[String] = {
expr.tree match {
case u.Apply(
u.TypeApply(
u.Select(u.Select(u.Ident(u.TermName("scala")), u.TermName("Some")), u.TermName("apply")),
List(u.TypeTree())),
str :: Nil
) if extractStringFromTree(str).nonEmpty =>
extractStringFromTree(str)
case u.Select(u.Ident(u.TermName("scala")), u.TermName("None")) =>
None
case _ =>
c.error(
expr.tree.pos,
"Parameter must be passed as an Option[String] literal such as " +
"Some(\"2.12.10\") or None")
None
}
}

def extractString(expr: c.Expr[String]): String = {
extractStringFromTree(expr.tree).getOrElse {
c.error(
expr.tree.pos,
"Parameter must be passed as a string literal such as \"2.12.10\"")
""
}
}

val meetsMinVersionRequirement = {
val minVersionOptValue = extractStringOption(minVersionOpt)

// Note: Unit tests do not test that this bound is inclusive rather
// than exclusive so be careful when changing this code not to
// accidentally make this an exclusive bound (see ScalaVersionTest for
// details)
minVersionOptValue.forall(version => Current >= ScalaVersion(version))
}

val meetsMaxVersionRequirement = {
val maxVersionOptValue = extractStringOption(maxVersionOpt)
// Note: Unit tests do not test that this bound is inclusive rather
// than exclusive so be careful when changing this code not to
// accidentally make this an exclusive bound (see ScalaVersionTest for
// details)
maxVersionOptValue.forall(version => Current <= ScalaVersion(version))
}

if (meetsMinVersionRequirement && meetsMaxVersionRequirement) {
c.parse(extractString(code))
} else {
u.EmptyTree
}
}
}

class ScalaVersion private(
private val superMajor: Int,
private val major: Int,
private val minor: Int
) extends Ordered[ScalaVersion] {
override def compare(that: ScalaVersion): Int = {
if (this.superMajor != that.superMajor) {
this.superMajor.compareTo(that.superMajor)
} else if (this.major != that.major) {
this.major.compareTo(that.major)
} else {
this.minor.compareTo(that.minor)
}
}

override def equals(obj: Any): Boolean = {
obj match {
case that: ScalaVersion =>
compare(that) == 0
case _ =>
false
}
}

override def toString: String = {
s"$superMajor.$major.$minor"
}
}
16 changes: 14 additions & 2 deletions third_party/dependency_analyzer/src/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,29 @@ scala_test(
"io/bazel/rulesscala/dependencyanalyzer/AstUsedJarFinderTest.scala",
],
jvm_flags = common_jvm_flags,
unused_dependency_checker_mode = "off",
deps = [
"//external:io_bazel_rules_scala/dependency/scala/scala_compiler",
"//external:io_bazel_rules_scala/dependency/scala/scala_library",
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
"//third_party/dependency_analyzer/src/main:dependency_analyzer",
"//third_party/dependency_analyzer/src/main:scala_version",
"//third_party/utils/src/test:test_util",
"@scalac_rules_commons_io//jar",
],
)

scala_test(
name = "scala_version_test",
size = "small",
srcs = [
"io/bazel/rulesscala/dependencyanalyzer/ScalaVersionTest.scala",
],
deps = [
"//external:io_bazel_rules_scala/dependency/scala/scala_library",
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
"//third_party/dependency_analyzer/src/main:scala_version",
],
)

scala_test(
name = "strict_deps_test",
size = "small",
Expand Down
Loading

0 comments on commit d534984

Please sign in to comment.