Skip to content

Commit 9161781

Browse files
Jamie5Andre Rocha
authored andcommitted
Include location in error report (bazel-contrib#988)
* Include location in error report * add test
1 parent db591fb commit 9161781

File tree

8 files changed

+191
-57
lines changed

8 files changed

+191
-57
lines changed

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

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,66 @@ class AstUsedJarFinder(
88
) {
99
import global._
1010

11-
def findUsedJars: Set[AbstractFile] = {
12-
val jars = collection.mutable.Set[AbstractFile]()
11+
def findUsedJars: Map[AbstractFile, Global#Position] = {
12+
val jars = collection.mutable.Map[AbstractFile, global.Position]()
1313

14-
def handleType(tpe: Type): Unit = {
14+
def recordUse(source: AbstractFile, pos: Position): Unit = {
15+
// We prefer to report locations which have information (e.g.
16+
// we don't want NoPosition).
17+
if (!jars.contains(source) || !jars(source).isDefined) {
18+
jars.put(source, pos)
19+
}
20+
}
21+
22+
def handleType(tpe: Type, pos: Position): Unit = {
1523
val sym = tpe.typeSymbol
1624
val assocFile = sym.associatedFile
1725
if (assocFile.path.endsWith(".class"))
1826
assocFile.underlyingSource.foreach { source =>
19-
jars.add(source)
27+
recordUse(source, pos)
2028
}
2129
}
2230

23-
def exploreType(tpe: Type): Unit = {
24-
handleType(tpe)
25-
tpe.typeArgs.foreach(exploreType)
31+
def exploreType(tpe: Type, pos: Position): Unit = {
32+
handleType(tpe, pos)
33+
tpe.typeArgs.foreach(exploreType(_, pos))
2634
}
2735

2836
def fullyExploreTree(tree: Tree): Unit = {
2937
exploreTree(tree)
3038
tree.foreach(exploreTree)
3139
}
3240

41+
def exploreClassfileAnnotArg(arg: ClassfileAnnotArg, pos: Position): Unit = {
42+
arg match {
43+
case LiteralAnnotArg(value) =>
44+
exploreConstant(value, pos)
45+
case ArrayAnnotArg(args) =>
46+
args.foreach(exploreClassfileAnnotArg(_, pos))
47+
case NestedAnnotArg(info) =>
48+
exploreAnnotationInfo(info)
49+
case _ =>
50+
}
51+
}
52+
def exploreAnnotationInfo(annot: AnnotationInfo): Unit = {
53+
// It would be nice if we could just do
54+
// fullyExploreTree(annot.tree)
55+
// Unfortunately that tree is synthetic and hence doesn't have
56+
// positions attached. Hence we examine the components that
57+
// go into that tree separately, as those do have positions.
58+
exploreType(annot.tpe, annot.pos)
59+
annot.scalaArgs.foreach(fullyExploreTree)
60+
annot.javaArgs.values.foreach(exploreClassfileAnnotArg(_, annot.pos))
61+
}
62+
63+
def exploreConstant(value: Constant, pos: Position): Unit = {
64+
value.value match {
65+
case tpe: Type =>
66+
exploreType(tpe, pos)
67+
case _ =>
68+
}
69+
}
70+
3371
def exploreTree(tree: Tree): Unit = {
3472
tree match {
3573
case node: TypeTree =>
@@ -51,11 +89,7 @@ class AstUsedJarFinder(
5189
"""
5290
)
5391

54-
node.value.value match {
55-
case tpe: Type =>
56-
exploreType(tpe)
57-
case _ =>
58-
}
92+
exploreConstant(node.value, tree.pos)
5993
case _ =>
6094
}
6195

@@ -69,19 +103,17 @@ class AstUsedJarFinder(
69103

70104
if (shouldExamine) {
71105
if (tree.hasSymbolField) {
72-
tree.symbol.annotations.foreach { annot =>
73-
annot.tree.foreach(fullyExploreTree)
74-
}
106+
tree.symbol.annotations.foreach(exploreAnnotationInfo)
75107
}
76108
if (tree.tpe != null) {
77-
exploreType(tree.tpe)
109+
exploreType(tree.tpe, tree.pos)
78110
}
79111
}
80112
}
81113

82114
currentRun.units.foreach { unit =>
83115
unit.body.foreach(fullyExploreTree)
84116
}
85-
jars.toSet
117+
jars.toMap
86118
}
87119
}

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

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -65,37 +65,63 @@ class DependencyAnalyzer(val global: Global) extends Plugin {
6565
}
6666

6767
private def runAnalysis(): Unit = {
68-
val usedJars = findUsedJars
69-
val usedJarPaths = if (!isWindows) usedJars.map(_.path) else usedJars.map(_.path.replaceAll("\\\\", "/"))
68+
val usedJarsToPositions = findUsedJarsAndPositions
69+
val usedJarPathToPositions =
70+
if (!isWindows) {
71+
usedJarsToPositions.map { case (jar, pos) =>
72+
jar.path -> pos
73+
}
74+
} else {
75+
usedJarsToPositions.map { case (jar, pos) =>
76+
jar.path.replaceAll("\\\\", "/") -> pos
77+
}
78+
}
7079

7180
if (settings.unusedDepsMode != AnalyzerMode.Off) {
72-
reportUnusedDepsFoundIn(usedJarPaths)
81+
reportUnusedDepsFoundIn(usedJarPathToPositions)
7382
}
7483

7584
if (settings.strictDepsMode != AnalyzerMode.Off) {
76-
reportIndirectTargetsFoundIn(usedJarPaths)
85+
reportIndirectTargetsFoundIn(usedJarPathToPositions)
7786
}
7887
}
7988

80-
private def reportIndirectTargetsFoundIn(usedJarPaths: Set[String]): Unit = {
89+
private def reportIndirectTargetsFoundIn(
90+
usedJarPathAndPositions: Map[String, global.Position]
91+
): Unit = {
8192
val errors =
82-
usedJarPaths
83-
.filterNot(settings.directTargetSet.jarSet.contains)
84-
.flatMap(settings.indirectTargetSet.targetFromJarOpt)
85-
.map { target =>
86-
s"""Target '$target' is used but isn't explicitly declared, please add it to the deps.
87-
|You can use the following buildozer command:
88-
|buildozer 'add deps $target' ${settings.currentTarget}""".stripMargin
93+
usedJarPathAndPositions
94+
.filterNot { case (jarPath, _) =>
95+
settings.directTargetSet.jarSet.contains(jarPath)
96+
}
97+
.flatMap { case (jarPath, pos) =>
98+
settings.indirectTargetSet.targetFromJarOpt(jarPath).map { target =>
99+
target -> pos
100+
}
101+
}
102+
.map { case (target, pos) =>
103+
val message =
104+
s"""Target '$target' is used but isn't explicitly declared, please add it to the deps.
105+
|You can use the following buildozer command:
106+
|buildozer 'add deps $target' ${settings.currentTarget}""".stripMargin
107+
message -> pos
89108
}
90109

91110
warnOrError(settings.strictDepsMode, errors)
92111
}
93112

94-
private def reportUnusedDepsFoundIn(usedJarPaths: Set[String]): Unit = {
113+
private def reportUnusedDepsFoundIn(
114+
usedJarPathAndPositions: Map[String, global.Position]
115+
): Unit = {
95116
val directJarPaths = settings.directTargetSet.jarSet
96117

118+
97119
val usedTargets =
98-
usedJarPaths.flatMap(settings.directTargetSet.targetFromJarOpt)
120+
usedJarPathAndPositions
121+
.flatMap { case (jar, _) =>
122+
settings.directTargetSet.targetFromJarOpt(jar)
123+
}
124+
.toSet
99125

100126
val unusedTargets = directJarPaths
101127
// This .get is safe because [jar] was gotten from [directJarPaths]
@@ -106,29 +132,44 @@ class DependencyAnalyzer(val global: Global) extends Plugin {
106132

107133
val toWarnOrError =
108134
unusedTargets.map { target =>
109-
s"""Target '$target' is specified as a dependency to ${settings.currentTarget} but isn't used, please remove it from the deps.
110-
|You can use the following buildozer command:
111-
|buildozer 'remove deps $target' ${settings.currentTarget}
112-
|""".stripMargin
135+
val message =
136+
s"""Target '$target' is specified as a dependency to ${settings.currentTarget} but isn't used, please remove it from the deps.
137+
|You can use the following buildozer command:
138+
|buildozer 'remove deps $target' ${settings.currentTarget}
139+
|""".stripMargin
140+
(message, global.NoPosition: global.Position)
113141
}
114142

115-
warnOrError(settings.unusedDepsMode, toWarnOrError)
143+
warnOrError(settings.unusedDepsMode, toWarnOrError.toMap)
116144
}
117145

118146
private def warnOrError(
119147
analyzerMode: AnalyzerMode,
120-
errors: Set[String]
148+
errors: Map[String, global.Position]
121149
): Unit = {
122-
val reportFunction: String => Unit = analyzerMode match {
123-
case AnalyzerMode.Error => global.reporter.error(global.NoPosition, _)
124-
case AnalyzerMode.Warn => global.reporter.warning(global.NoPosition, _)
125-
case AnalyzerMode.Off => _ => ()
150+
val reportFunction: (String, global.Position) => Unit = analyzerMode match {
151+
case AnalyzerMode.Error =>
152+
{ case (message, pos) =>
153+
global.reporter.error(pos, message)
154+
}
155+
case AnalyzerMode.Warn =>
156+
{ case (message, pos) =>
157+
global.reporter.warning(pos, message)
158+
}
159+
case AnalyzerMode.Off => (_, _) => ()
126160
}
127161

128-
errors.foreach(reportFunction)
162+
errors.foreach { case (message, pos) =>
163+
reportFunction(message, pos)
164+
}
129165
}
130166

131-
private def findUsedJars: Set[AbstractFile] = {
167+
/**
168+
*
169+
* @return map of used jar file -> representative position in file where
170+
* it was used
171+
*/
172+
private def findUsedJarsAndPositions: Map[AbstractFile, global.Position] = {
132173
settings.dependencyTrackingMethod match {
133174
case DependencyTrackingMethod.HighLevel =>
134175
new HighLevelCrawlUsedJarFinder(global).findUsedJars

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ class HighLevelCrawlUsedJarFinder(
88
) {
99
import global.Symbol
1010

11-
def findUsedJars: Set[AbstractFile] = {
11+
def findUsedJars: Map[AbstractFile, Global#Position] = {
1212
val jars = collection.mutable.Set[AbstractFile]()
1313

1414
global.exitingTyper {
1515
walkTopLevels(global.RootClass, jars)
1616
}
17-
jars.toSet
17+
jars.map(jar => jar -> global.NoPosition).toMap
1818
}
1919

2020
private def walkTopLevels(root: Symbol, jars: collection.mutable.Set[AbstractFile]): Unit = {

third_party/dependency_analyzer/src/test/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ scala_test(
1515
],
1616
jvm_flags = common_jvm_flags,
1717
deps = [
18+
"//external:io_bazel_rules_scala/dependency/scala/scala_compiler",
1819
"//external:io_bazel_rules_scala/dependency/scala/scala_library",
1920
"//external:io_bazel_rules_scala/dependency/scala/scala_reflect",
2021
"//third_party/dependency_analyzer/src/main:dependency_analyzer",

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

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import java.nio.file.Files
44
import java.nio.file.Path
55
import org.apache.commons.io.FileUtils
66
import org.scalatest._
7+
import scala.tools.nsc.reporters.StoreReporter
78
import third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer.DependencyTrackingMethod
89
import third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer.ScalaVersion
910
import third_party.utils.src.test.io.bazel.rulesscala.utils.JavaCompileUtil
@@ -29,11 +30,13 @@ class AstUsedJarFinderTest extends FunSuite {
2930
def compileWithoutAnalyzer(
3031
code: String
3132
): Unit = {
32-
TestUtil.runCompiler(
33-
code = code,
34-
extraClasspath = List(tmpDir.toString),
35-
outputPathOpt = Some(tmpDir)
36-
)
33+
val errors =
34+
TestUtil.runCompiler(
35+
code = code,
36+
extraClasspath = List(tmpDir.toString),
37+
outputPathOpt = Some(tmpDir)
38+
)
39+
assert(errors.isEmpty)
3740
}
3841

3942
def compileJava(
@@ -50,7 +53,7 @@ class AstUsedJarFinderTest extends FunSuite {
5053
def checkStrictDepsErrorsReported(
5154
code: String,
5255
expectedStrictDeps: List[String]
53-
): Unit = {
56+
): List[StoreReporter#Info] = {
5457
val errors =
5558
TestUtil.runCompiler(
5659
code = code,
@@ -67,11 +70,17 @@ class AstUsedJarFinderTest extends FunSuite {
6770
)
6871

6972
assert(errors.size == expectedStrictDeps.size)
73+
errors.foreach { err =>
74+
// We should be emitting errors with positions
75+
assert(err.pos.isDefined)
76+
}
7077

7178
expectedStrictDeps.foreach { dep =>
7279
val expectedError = s"Target '$dep' is used but isn't explicitly declared, please add it to the deps"
73-
assert(errors.exists(_.contains(expectedError)))
80+
assert(errors.exists(_.msg.contains(expectedError)))
7481
}
82+
83+
errors
7584
}
7685

7786
def checkUnusedDepsErrorReported(
@@ -94,10 +103,14 @@ class AstUsedJarFinderTest extends FunSuite {
94103
)
95104

96105
assert(errors.size == expectedUnusedDeps.size)
106+
errors.foreach { err =>
107+
// As an unused dep we shouldn't include a position or anything like that
108+
assert(!err.pos.isDefined)
109+
}
97110

98111
expectedUnusedDeps.foreach { dep =>
99112
val expectedError = s"Target '$dep' is specified as a dependency to ${TestUtil.defaultTarget} but isn't used, please remove it from the deps."
100-
assert(errors.exists(_.contains(expectedError)))
113+
assert(errors.exists(_.msg.contains(expectedError)))
101114
}
102115
}
103116
}
@@ -452,4 +465,44 @@ class AstUsedJarFinderTest extends FunSuite {
452465
}
453466
}
454467
}
468+
469+
test("classOf in class Java annotation is direct") {
470+
withSandbox { sandbox =>
471+
sandbox.compileJava(
472+
className = "Category",
473+
code =
474+
s"""
475+
|public @interface Category {
476+
| Class<?> value();
477+
|}
478+
|""".stripMargin
479+
)
480+
sandbox.compileWithoutAnalyzer("class UnitTests")
481+
sandbox.checkStrictDepsErrorsReported(
482+
"""
483+
|@Category(classOf[UnitTests])
484+
|class C
485+
|""".stripMargin,
486+
expectedStrictDeps = List("UnitTests", "Category")
487+
)
488+
}
489+
}
490+
491+
test("position of strict deps error is correct") {
492+
// While we do ensure that generally strict deps errors have
493+
// a position in the other tests, here we make sure that that
494+
// position is correctly computed.
495+
withSandbox { sandbox =>
496+
sandbox.compileWithoutAnalyzer("class A")
497+
val errors =
498+
sandbox.checkStrictDepsErrorsReported(
499+
"class B(a: A)",
500+
expectedStrictDeps = List("A")
501+
)
502+
assert(errors.size == 1)
503+
val pos = errors(0).pos
504+
assert(pos.line == 1)
505+
assert(pos.column == 12)
506+
}
507+
}
455508
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class StrictDepsTest extends FunSuite {
2525
)
2626
)
2727
)
28+
.map(_.msg)
2829
}
2930

3031
test("error on indirect dependency target") {

0 commit comments

Comments
 (0)