From a36d8401e11b5e97e79f9b8f1818da1dafe4aced Mon Sep 17 00:00:00 2001 From: jvican Date: Tue, 14 Mar 2017 15:40:45 +0100 Subject: [PATCH 1/4] Don't use runtime universe to discover autoImport The previous implementation was using the Scala runtime universe to check whether a plugin had or not an `autoImport` member. This is a bad idea for the following reasons: * The first time you use it, you class load the whole Scalac compiler universe. Not efficient. Measurements say this is about a second. * There is a small overhead of going through the reflection API. There exists a better approach that consists in checking if `autoImport` exists with pure Java reflection. Since the class is already class loaded, we check for: * A class file named after the plugin FQN that includes `autoImport$` at the end, which means that an object named `autoImport` exists. * A field in the plugin class that is named `autoImport`. This complies with the plugin sbt specification: http://www.scala-sbt.org/1.0/docs/Plugins.html#Controlling+the+import+with+autoImport --- main/src/main/scala/sbt/Plugins.scala | 34 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/main/src/main/scala/sbt/Plugins.scala b/main/src/main/scala/sbt/Plugins.scala index 5b6cf57fe..6f4341ce9 100644 --- a/main/src/main/scala/sbt/Plugins.scala +++ b/main/src/main/scala/sbt/Plugins.scala @@ -349,18 +349,34 @@ ${listConflicts(conflicting)}""") case ap: AutoPlugin => model(ap) } + private val autoImport = "autoImport" + + /** Determines whether a plugin has a stable autoImport member by: + * + * 1. Checking whether there exists a public field. + * 2. Checking whether there exists a public object. + * + * The above checks work for inherited members too. + * + * @param ap The found plugin. + * @param loader The plugin loader. + * @return True if plugin has a stable member `autoImport`, otherwise false. + */ private[sbt] def hasAutoImportGetter(ap: AutoPlugin, loader: ClassLoader): Boolean = { - import reflect.runtime.{ universe => ru } + import java.lang.reflect.Field import scala.util.control.Exception.catching - val m = ru.runtimeMirror(loader) - val im = m.reflect(ap) - val hasGetterOpt = catching(classOf[ScalaReflectionException]) opt { - im.symbol.asType.toType.decl(ru.TermName("autoImport")) match { - case ru.NoSymbol => false - case sym => sym.asTerm.isGetter || sym.asTerm.isModule - } + // Make sure that we don't detect user-defined methods called autoImport + def existsAutoImportVal(clazz: Class[_]): Option[Field] = { + catching(classOf[NoSuchFieldException]) + .opt(clazz.getDeclaredField(autoImport)) + .orElse(Option(clazz.getSuperclass).flatMap(existsAutoImportVal)) } - hasGetterOpt getOrElse false + + val pluginClazz = ap.getClass + existsAutoImportVal(pluginClazz) + .orElse(catching(classOf[ClassNotFoundException]).opt( + Class.forName(s"${pluginClazz.getName}$autoImport$$", false, loader))) + .isDefined } /** Debugging method to time how long it takes to run various compilation tasks. */ From 3e812dc71a63da832b8fcf4a841925cdb578a859 Mon Sep 17 00:00:00 2001 From: jvican Date: Tue, 18 Apr 2017 16:17:20 +0200 Subject: [PATCH 2/4] Add global toolbox to parse sbt files This change was proposed by Jason in case that the new parsing mechanism implemented later on has to be reverted. This change provides a good baseline, but it's far from ideal with regard to readability of the parser and performance. --- .../scala/sbt/internal/parser/SbtParser.scala | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/main/src/main/scala/sbt/internal/parser/SbtParser.scala b/main/src/main/scala/sbt/internal/parser/SbtParser.scala index 5c81bd9f2..0422769bb 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -16,6 +16,14 @@ private[sbt] object SbtParser { private[parser] val NOT_FOUND_INDEX = -1 private[sbt] val FAKE_FILE = new File("fake") private[parser] val XML_ERROR = "';' expected but 'val' found." + + import scala.reflect.runtime._ + import scala.tools.reflect.ToolBox + private[parser] lazy val toolbox = + universe.rootMirror.mkToolBox(options = "-Yrangepos") + private[parser] def parse(code: String) = synchronized { + toolbox.parse(code) + } } /** @@ -60,24 +68,21 @@ private[sbt] case class SbtParser(file: File, lines: Seq[String]) extends Parsed val (imports, settings, settingsTrees) = splitExpressions(file, lines) private def splitExpressions(file: File, lines: Seq[String]): (Seq[(String, Int)], Seq[(String, LineRange)], Seq[(String, Tree)]) = { - import sbt.internal.parser.MissingBracketHandler._ + import sbt.internals.parser.MissingBracketHandler.findMissingText import scala.compat.Platform.EOL - import scala.reflect.runtime._ - import scala.tools.reflect.{ ToolBox, ToolBoxError } + import scala.tools.reflect.ToolBoxError - val mirror = universe.runtimeMirror(this.getClass.getClassLoader) - val toolbox = mirror.mkToolBox(options = "-Yrangepos") val indexedLines = lines.toIndexedSeq val content = indexedLines.mkString(END_OF_LINE) val fileName = file.getAbsolutePath val parsed = try { - toolbox.parse(content) + SbtParser.parse(content) } catch { case e: ToolBoxError => - val seq = toolbox.frontEnd.infos.map { i => + val seq = SbtParser.toolbox.frontEnd.infos.map { i => s"""[$fileName]:${i.pos.line}: ${i.msg}""" } val errorMessage = seq.mkString(EOL) @@ -98,6 +103,8 @@ private[sbt] case class SbtParser(file: File, lines: Seq[String]) extends Parsed errorMessage } throw new MessageOnlyException(error) + } finally { + SbtParser.toolbox.frontEnd.infos.clear() } val parsedTrees = parsed match { case Block(stmt, expr) => @@ -132,7 +139,7 @@ private[sbt] case class SbtParser(file: File, lines: Seq[String]) extends Parsed * @return originalStatement or originalStatement with missing bracket */ def parseStatementAgain(t: Tree, originalStatement: String): String = { - val statement = scala.util.Try(toolbox.parse(originalStatement)) match { + val statement = scala.util.Try(SbtParser.parse(originalStatement)) match { case scala.util.Failure(th) => val missingText = findMissingText(content, t.pos.end, t.pos.line, fileName, th) originalStatement + missingText From f482a6cf0d5b3df9e4246fa6456b070829b46d02 Mon Sep 17 00:00:00 2001 From: jvican Date: Wed, 15 Mar 2017 16:12:00 +0100 Subject: [PATCH 3/4] Reuse the same global instance for parsing The previous implementation was instantiating a toolbox to parse every time it parsed a sbt file (and even recursively!!!). This is inefficient and translates to instantiating a `ReflectGlobal` every time we want to parse something. This commit takes another approach: 1. It removes the dependency on `ReflectGlobal`. 2. It reuses the same `Global` and `Run` instances for parsing. This is an efficient as it can get without doing a whole overhaul of it. I think that in the future we may want to reimplement it to avoid the recursive parsing to work around Scalac's bug. --- .../scala/sbt/internal/parser/SbtParser.scala | 154 +++++++++++------- .../sbt/internal/parser/SbtRefactorings.scala | 5 +- 2 files changed, 101 insertions(+), 58 deletions(-) diff --git a/main/src/main/scala/sbt/internal/parser/SbtParser.scala b/main/src/main/scala/sbt/internal/parser/SbtParser.scala index 0422769bb..83d1ee610 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -8,7 +8,14 @@ import java.io.File import sbt.internal.parser.SbtParser._ -import scala.reflect.runtime.universe._ +import scala.compat.Platform.EOL +import scala.reflect.internal.util.BatchSourceFile +import scala.reflect.io.VirtualDirectory +import scala.reflect.internal.Positions +import scala.tools.nsc.{ CompilerCommand, Global } +import scala.tools.nsc.reporters.StoreReporter + +import scala.util.{Success, Failure} private[sbt] object SbtParser { val END_OF_LINE_CHAR = '\n' @@ -17,12 +24,85 @@ private[sbt] object SbtParser { private[sbt] val FAKE_FILE = new File("fake") private[parser] val XML_ERROR = "';' expected but 'val' found." - import scala.reflect.runtime._ - import scala.tools.reflect.ToolBox - private[parser] lazy val toolbox = - universe.rootMirror.mkToolBox(options = "-Yrangepos") - private[parser] def parse(code: String) = synchronized { - toolbox.parse(code) + private val XmlErrorMessage = + """Probably problem with parsing xml group, please add parens or semicolons: + |Replace: + |val xmlGroup = + |with: + |val xmlGroup = () + |or + |val xmlGroup = ; + """.stripMargin + + private final val defaultClasspath = + sbt.io.Path.makeString(sbt.io.IO.classLocationFile[Product] :: Nil) + + /** + * Provides the previous error reporting functionality in + * [[scala.tools.reflect.ToolBox]]. + * + * This is a sign that this whole parser should be rewritten. + * There are exceptions everywhere and the logic to work around + * the scalac parser bug heavily relies on them and it's tied + * to the test suite. Ideally, we only want to throw exceptions + * when we know for a fact that the user-provided snippet doesn't + * parse. + */ + private[sbt] class ParserStoreReporter extends StoreReporter { + def throwParserErrorsIfAny(fileName: String): Unit = { + if (parserReporter.hasErrors) { + val seq = parserReporter.infos.map { info => + s"""[$fileName]:${info.pos.line}: ${info.msg}""" + } + val errorMessage = seq.mkString(EOL) + val error: String = + if (errorMessage.contains(XML_ERROR)) + s"$errorMessage\n${SbtParser.XmlErrorMessage}" + else errorMessage + throw new MessageOnlyException(error) + } + } + } + + private[sbt] final val parserReporter = new ParserStoreReporter + + private[sbt] final lazy val defaultGlobalForParser = { + import scala.reflect.internal.util.NoPosition + val options = "-cp" :: s"$defaultClasspath" :: "-Yrangepos" :: Nil + val reportError = (msg: String) => parserReporter.error(NoPosition, msg) + val command = new CompilerCommand(options, reportError) + val settings = command.settings + settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) + + // Mixing Positions is necessary, otherwise global ignores -Yrangepos + val global = new Global(settings, parserReporter) with Positions + val run = new global.Run + // Necessary to have a dummy unit for initialization... + val initFile = new BatchSourceFile("", "") + val _ = new global.CompilationUnit(initFile) + global.phase = run.parserPhase + global + } + + import defaultGlobalForParser.Tree + + /** + * Parse code reusing the same [[Run]] instance. + * + * The access to this method has to be synchronized (no more than one + * thread can access to it at the same time since it reuses the same + * [[Global]], which mutates the whole universe). + */ + private[sbt] def parse(code: String, fileName: String): Seq[Tree] = synchronized { + import defaultGlobalForParser._ + parserReporter.reset() + val wrapperFile = new BatchSourceFile("", code) + val unit = new CompilationUnit(wrapperFile) + val parser = new syntaxAnalyzer.UnitParser(unit) + val parsedTrees = parser.templateStats() + parser.accept(scala.tools.nsc.ast.parser.Tokens.EOF) + parserReporter.throwParserErrorsIfAny(fileName) + parsedTrees } } @@ -38,7 +118,7 @@ sealed trait ParsedSbtFileExpressions { def settings: Seq[(String, LineRange)] /** The set of scala tree's for parsed definitions/settings and the underlying string representation.. */ - def settingsTrees: Seq[(String, Tree)] + def settingsTrees: Seq[(String, Global#Tree)] } @@ -67,56 +147,20 @@ private[sbt] case class SbtParser(file: File, lines: Seq[String]) extends Parsed // parsed trees. val (imports, settings, settingsTrees) = splitExpressions(file, lines) - private def splitExpressions(file: File, lines: Seq[String]): (Seq[(String, Int)], Seq[(String, LineRange)], Seq[(String, Tree)]) = { - import sbt.internals.parser.MissingBracketHandler.findMissingText + import SbtParser.defaultGlobalForParser._ - import scala.compat.Platform.EOL - import scala.tools.reflect.ToolBoxError + private def splitExpressions(file: File, lines: Seq[String]): (Seq[(String, Int)], Seq[(String, LineRange)], Seq[(String, Tree)]) = { + import sbt.internal.parser.MissingBracketHandler.findMissingText val indexedLines = lines.toIndexedSeq val content = indexedLines.mkString(END_OF_LINE) val fileName = file.getAbsolutePath - - val parsed = - try { - SbtParser.parse(content) - } catch { - case e: ToolBoxError => - val seq = SbtParser.toolbox.frontEnd.infos.map { i => - s"""[$fileName]:${i.pos.line}: ${i.msg}""" - } - val errorMessage = seq.mkString(EOL) - - val error = if (errorMessage.contains(XML_ERROR)) { - s""" - |$errorMessage - |Probably problem with parsing xml group, please add parens or semicolons: - |Replace: - |val xmlGroup = - |with: - |val xmlGroup = () - |or - |val xmlGroup = ; - | - """.stripMargin - } else { - errorMessage - } - throw new MessageOnlyException(error) - } finally { - SbtParser.toolbox.frontEnd.infos.clear() - } - val parsedTrees = parsed match { - case Block(stmt, expr) => - stmt :+ expr - case t: Tree => - Seq(t) - } + val parsedTrees: Seq[Tree] = parse(content, fileName) // Check No val (a,b) = foo *or* val a,b = foo as these are problematic to range positions and the WHOLE architecture. def isBadValDef(t: Tree): Boolean = t match { - case x @ toolbox.u.ValDef(_, _, _, rhs) if rhs != toolbox.u.EmptyTree => + case x @ ValDef(_, _, _, rhs) if rhs != EmptyTree => val c = content.substring(x.pos.start, x.pos.end) !(c contains "=") case _ => false @@ -127,7 +171,7 @@ private[sbt] case class SbtParser(file: File, lines: Seq[String]) extends Parsed throw new MessageOnlyException(s"""[$fileName]:$positionLine: Pattern matching in val statements is not supported""".stripMargin) } - val (imports, statements) = parsedTrees partition { + val (imports: Seq[Tree], statements: Seq[Tree]) = parsedTrees partition { case _: Import => true case _ => false } @@ -139,8 +183,8 @@ private[sbt] case class SbtParser(file: File, lines: Seq[String]) extends Parsed * @return originalStatement or originalStatement with missing bracket */ def parseStatementAgain(t: Tree, originalStatement: String): String = { - val statement = scala.util.Try(SbtParser.parse(originalStatement)) match { - case scala.util.Failure(th) => + val statement = scala.util.Try(parse(originalStatement, fileName)) match { + case Failure(th) => val missingText = findMissingText(content, t.pos.end, t.pos.line, fileName, th) originalStatement + missingText case _ => @@ -224,10 +268,10 @@ private[sbt] object MissingBracketHandler { case Some(index) => val text = content.substring(positionEnd, index + 1) val textWithoutBracket = text.substring(0, text.length - 1) - scala.util.Try(SbtParser(FAKE_FILE, textWithoutBracket.lines.toSeq)) match { - case scala.util.Success(_) => + scala.util.Try(SbtParser.parse(textWithoutBracket, fileName)) match { + case Success(_) => text - case scala.util.Failure(th) => + case Failure(th) => findMissingText(content, index + 1, positionLine, fileName, originalException) } case _ => diff --git a/main/src/main/scala/sbt/internal/parser/SbtRefactorings.scala b/main/src/main/scala/sbt/internal/parser/SbtRefactorings.scala index e90fa1818..0a657e7b0 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtRefactorings.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtRefactorings.scala @@ -2,8 +2,6 @@ package sbt package internal package parser -import scala.reflect.runtime.universe._ - private[sbt] object SbtRefactorings { import sbt.internal.parser.SbtParser.{ END_OF_LINE, FAKE_FILE } @@ -80,7 +78,8 @@ private[sbt] object SbtRefactorings { seq.toMap } - private def extractSettingName(tree: Tree): String = + import scala.tools.nsc.Global + private def extractSettingName(tree: Global#Tree): String = tree.children match { case h :: _ => extractSettingName(h) From 2f61114108557bda58d93382fe9696c887c67110 Mon Sep 17 00:00:00 2001 From: jvican Date: Wed, 15 Mar 2017 19:21:14 +0100 Subject: [PATCH 4/4] Avoid the use of `synchronized` while parsing Previous commit used `synchronized` to ensure that the global reporter was not reporting errors from other parsing sessions. Theoretically, though, sbt could invoke parsing in parallel, so it's better to ensure we remove the `synchronized` block, which could also be preventing some JVM optimizations. The following commit solves the issue by introducing a reporter id. A reporter id is a unique identifier that is mapped to a reporter. Every parsing session gets its own identifier, which then is reused for recursive parsing. Error reports between recursive parses cannot collide because the reporter is cleaned in `parse`. --- .../scala/sbt/internal/parser/SbtParser.scala | 97 +++++++++++++------ 1 file changed, 69 insertions(+), 28 deletions(-) diff --git a/main/src/main/scala/sbt/internal/parser/SbtParser.scala b/main/src/main/scala/sbt/internal/parser/SbtParser.scala index 83d1ee610..9d961e110 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -5,15 +5,17 @@ package parser import sbt.internal.util.{ LineRange, MessageOnlyException } import java.io.File +import java.util.concurrent.ConcurrentHashMap import sbt.internal.parser.SbtParser._ import scala.compat.Platform.EOL -import scala.reflect.internal.util.BatchSourceFile +import scala.reflect.internal.util.{ BatchSourceFile, Position } import scala.reflect.io.VirtualDirectory import scala.reflect.internal.Positions import scala.tools.nsc.{ CompilerCommand, Global } -import scala.tools.nsc.reporters.StoreReporter +import scala.tools.nsc.reporters.{ Reporter, StoreReporter } +import scala.util.Random import scala.util.{Success, Failure} @@ -41,17 +43,49 @@ private[sbt] object SbtParser { * Provides the previous error reporting functionality in * [[scala.tools.reflect.ToolBox]]. * - * This is a sign that this whole parser should be rewritten. + * This parser is a wrapper around a collection of reporters that are + * indexed by a unique key. This is used to ensure that the reports of + * one parser don't collide with other ones in concurrent settings. + * + * This parser is a sign that this whole parser should be rewritten. * There are exceptions everywhere and the logic to work around * the scalac parser bug heavily relies on them and it's tied * to the test suite. Ideally, we only want to throw exceptions * when we know for a fact that the user-provided snippet doesn't * parse. */ - private[sbt] class ParserStoreReporter extends StoreReporter { - def throwParserErrorsIfAny(fileName: String): Unit = { - if (parserReporter.hasErrors) { - val seq = parserReporter.infos.map { info => + private[sbt] class UniqueParserReporter extends Reporter { + + private val reporters = new ConcurrentHashMap[String, StoreReporter]() + + override def info0(pos: Position, msg: String, severity: Severity, force: Boolean): Unit = { + val reporter = getReporter(pos.source.file.name) + severity.id match { + case 0 => reporter.info(pos, msg, force) + case 1 => reporter.warning(pos, msg) + case 2 => reporter.error(pos, msg) + } + } + + def getOrCreateReporter(uniqueFileName: String): StoreReporter = { + val reporter = reporters.get(uniqueFileName) + if (reporter == null) { + val newReporter = new StoreReporter + reporters.put(uniqueFileName, newReporter) + newReporter + } else reporter + } + + private def getReporter(fileName: String) = { + val reporter = reporters.get(fileName) + if (reporter == null) + sys.error(s"Sbt parser failure: no reporter for $fileName.") + reporter + } + + def throwParserErrorsIfAny(reporter: StoreReporter, fileName: String): Unit = { + if (reporter.hasErrors) { + val seq = reporter.infos.map { info => s"""[$fileName]:${info.pos.line}: ${info.msg}""" } val errorMessage = seq.mkString(EOL) @@ -60,24 +94,24 @@ private[sbt] object SbtParser { s"$errorMessage\n${SbtParser.XmlErrorMessage}" else errorMessage throw new MessageOnlyException(error) - } + } else () } } - private[sbt] final val parserReporter = new ParserStoreReporter + private[sbt] final val globalReporter = new UniqueParserReporter private[sbt] final lazy val defaultGlobalForParser = { import scala.reflect.internal.util.NoPosition val options = "-cp" :: s"$defaultClasspath" :: "-Yrangepos" :: Nil - val reportError = (msg: String) => parserReporter.error(NoPosition, msg) + val reportError = (msg: String) => globalReporter.error(NoPosition, msg) val command = new CompilerCommand(options, reportError) val settings = command.settings settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) - // Mixing Positions is necessary, otherwise global ignores -Yrangepos - val global = new Global(settings, parserReporter) with Positions + // Mix Positions, otherwise global ignores -Yrangepos + val global = new Global(settings, globalReporter) with Positions val run = new global.Run - // Necessary to have a dummy unit for initialization... + // Add required dummy unit for initialization... val initFile = new BatchSourceFile("", "") val _ = new global.CompilationUnit(initFile) global.phase = run.parserPhase @@ -89,20 +123,27 @@ private[sbt] object SbtParser { /** * Parse code reusing the same [[Run]] instance. * - * The access to this method has to be synchronized (no more than one - * thread can access to it at the same time since it reuses the same - * [[Global]], which mutates the whole universe). + * @param code The code to be parsed. + * @param filePath The file name where the code comes from. + * @param reporterId0 The reporter id is the key used to get the pertinent + * reporter. Given that the parsing reuses a global + * instance, this reporter id makes sure that every parsing + * session gets its own errors in a concurrent setting. + * The reporter id must be unique per parsing session. + * @return */ - private[sbt] def parse(code: String, fileName: String): Seq[Tree] = synchronized { + private[sbt] def parse(code: String, filePath: String, reporterId0: Option[String]): (Seq[Tree], String) = { import defaultGlobalForParser._ - parserReporter.reset() - val wrapperFile = new BatchSourceFile("", code) + val reporterId = reporterId0.getOrElse(s"$filePath-${Random.nextInt}") + val reporter = globalReporter.getOrCreateReporter(reporterId) + reporter.reset() + val wrapperFile = new BatchSourceFile(reporterId, code) val unit = new CompilationUnit(wrapperFile) val parser = new syntaxAnalyzer.UnitParser(unit) val parsedTrees = parser.templateStats() parser.accept(scala.tools.nsc.ast.parser.Tokens.EOF) - parserReporter.throwParserErrorsIfAny(fileName) - parsedTrees + globalReporter.throwParserErrorsIfAny(reporter, filePath) + parsedTrees -> reporterId } } @@ -155,7 +196,7 @@ private[sbt] case class SbtParser(file: File, lines: Seq[String]) extends Parsed val indexedLines = lines.toIndexedSeq val content = indexedLines.mkString(END_OF_LINE) val fileName = file.getAbsolutePath - val parsedTrees: Seq[Tree] = parse(content, fileName) + val (parsedTrees, reporterId) = parse(content, fileName, None) // Check No val (a,b) = foo *or* val a,b = foo as these are problematic to range positions and the WHOLE architecture. def isBadValDef(t: Tree): Boolean = @@ -183,9 +224,9 @@ private[sbt] case class SbtParser(file: File, lines: Seq[String]) extends Parsed * @return originalStatement or originalStatement with missing bracket */ def parseStatementAgain(t: Tree, originalStatement: String): String = { - val statement = scala.util.Try(parse(originalStatement, fileName)) match { + val statement = scala.util.Try(parse(originalStatement, fileName, Some(reporterId))) match { case Failure(th) => - val missingText = findMissingText(content, t.pos.end, t.pos.line, fileName, th) + val missingText = findMissingText(content, t.pos.end, t.pos.line, fileName, th, Some(reporterId)) originalStatement + missingText case _ => originalStatement @@ -263,16 +304,16 @@ private[sbt] object MissingBracketHandler { * @param originalException - original exception * @return missing text */ - private[sbt] def findMissingText(content: String, positionEnd: Int, positionLine: Int, fileName: String, originalException: Throwable): String = { + private[sbt] def findMissingText(content: String, positionEnd: Int, positionLine: Int, fileName: String, originalException: Throwable, reporterId: Option[String] = Some(Random.nextInt.toString)): String = { findClosingBracketIndex(content, positionEnd) match { case Some(index) => val text = content.substring(positionEnd, index + 1) val textWithoutBracket = text.substring(0, text.length - 1) - scala.util.Try(SbtParser.parse(textWithoutBracket, fileName)) match { + scala.util.Try(SbtParser.parse(textWithoutBracket, fileName, reporterId)) match { case Success(_) => text - case Failure(th) => - findMissingText(content, index + 1, positionLine, fileName, originalException) + case Failure(_) => + findMissingText(content, index + 1, positionLine, fileName, originalException, reporterId) } case _ => throw new MessageOnlyException(s"""[$fileName]:$positionLine: ${originalException.getMessage}""".stripMargin)