Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: use pc for go to def when stale semanticdb #7028

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.{util => ju}

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.Try

import scala.meta.inputs.Input
import scala.meta.inputs.Position.Range
Expand Down Expand Up @@ -59,7 +60,6 @@ final class DefinitionProvider(
scalaVersionSelector: ScalaVersionSelector,
saveDefFileToDisk: Boolean,
sourceMapper: SourceMapper,
warnings: () => Warnings,
)(implicit ec: ExecutionContext, rc: ReportContext) {

private val fallback = new FallbackDefinitionProvider(trees, index)
Expand All @@ -78,48 +78,77 @@ final class DefinitionProvider(
val scaladocDefinitionProvider =
new ScaladocDefinitionProvider(buffers, trees, destinationProvider)

private def isAmmonnite(path: AbsolutePath): Boolean =
path.isAmmoniteScript && buildTargets
.inverseSources(path)
.flatMap(buildTargets.targetData)
.exists(_.isAmmonite)

def definition(
path: AbsolutePath,
params: TextDocumentPositionParams,
token: CancelToken,
): Future[DefinitionResult] = {
val fromSemanticdb =
semanticdbs().textDocument(path).documentIncludingStale
val fromSnapshot = fromSemanticdb match {
case Some(doc) =>
definitionFromSnapshot(path, params, doc)
case _ =>
DefinitionResult.empty
}
val fromCompilerOrSemanticdb =
fromSnapshot match {
case defn if defn.isEmpty && path.isScalaFilename =>
compilers().definition(params, token)
case defn @ DefinitionResult(_, symbol, _, _, querySymbol)
if symbol != querySymbol && path.isScalaFilename =>
compilers().definition(params, token).map { compilerDefn =>
if (compilerDefn.isEmpty || compilerDefn.querySymbol == querySymbol)
defn
else compilerDefn.copy(semanticdb = defn.semanticdb)
val reportBuilder = new DefinitionProviderReportBuilder(path, params)
lazy val isScala3 = ScalaVersions.isScala3Version(
scalaVersionSelector.scalaVersionForPath(path)
)

def fromCompiler() =
if (path.isScalaFilename) {
compilers()
.definition(params, token)
.map(reportBuilder.withCompilerResult)
.map {
case res if res.isEmpty => Some(res)
case res =>
val pathToDef = res.locations.asScala.head.getUri.toAbsolutePath
Some(
res.copy(semanticdb =
semanticdbs().textDocument(pathToDef).documentIncludingStale
)
)
}
case defn =>
if (fromSemanticdb.isEmpty) {
warnings().noSemanticdb(path)
} else Future.successful(None)

def fromSemanticDb() = Future.successful {
semanticdbs()
.textDocument(path)
.documentIncludingStale
.map(definitionFromSnapshot(path, params, _))
.map(reportBuilder.withSemanticDBResult)
}

def fromScalaDoc() = Future.successful {
scaladocDefinitionProvider
.definition(path, params, isScala3)
.map(reportBuilder.withFoundScaladocDef)
}

def fromFallback() =
Future.successful(
fallback
.search(path, params.getPosition(), isScala3, reportBuilder)
.map(reportBuilder.withFallbackResult)
)

val strategies: List[() => Future[Option[DefinitionResult]]] =
if (isAmmonnite(path))
List(fromSemanticDb, fromCompiler, fromScalaDoc, fromFallback)
else List(fromCompiler, fromSemanticDb, fromScalaDoc, fromFallback)

for {
result <- strategies.foldLeft(Future.successful(DefinitionResult.empty)) {
case (acc, next) =>
acc.flatMap {
case res if res.isEmpty && !res.symbol.endsWith("/") =>
next().map(_.getOrElse(res))
case res => Future.successful(res)
}
Future.successful(defn)
}

fromCompilerOrSemanticdb.map { definition =>
if (definition.isEmpty && !definition.symbol.endsWith("/")) {
val isScala3 =
ScalaVersions.isScala3Version(
scalaVersionSelector.scalaVersionForPath(path)
)
scaladocDefinitionProvider
.definition(path, params, isScala3)
.orElse(fallback.search(path, params.getPosition(), isScala3))
.getOrElse(definition)
} else definition
} yield {
reportBuilder.build().foreach(rc.unsanitized.create(_))
result
}
}

Expand Down Expand Up @@ -508,3 +537,96 @@ class DestinationProvider(
}
}
}

class DefinitionProviderReportBuilder(
path: AbsolutePath,
params: TextDocumentPositionParams,
) {
private var compilerDefn: Option[DefinitionResult] = None
private var semanticDBDefn: Option[DefinitionResult] = None

private var fallbackDefn: Option[DefinitionResult] = None
private var nonLocalGuesses: List[String] = List.empty

private var fundScaladocDef = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private var fundScaladocDef = false
private var foundScaladocDef = false


private var error: Option[Throwable] = None

def withCompilerResult(result: DefinitionResult): DefinitionResult = {
compilerDefn = Some(result)
result
}

def withSemanticDBResult(result: DefinitionResult): DefinitionResult = {
semanticDBDefn = Some(result)
result
}

def withFallbackResult(result: DefinitionResult): DefinitionResult = {
fallbackDefn = Some(result)
result
}

def withError(e: Throwable): Unit = {
error = Some(e)
}

def withNonLocalGuesses(guesses: List[String]): Unit = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def withNonLocalGuesses(guesses: List[String]): Unit = {
def setNonLocalGuesses(guesses: List[String]): Unit = {

This is more a setter than a builder method, no? Could we make them more consistent or rename as above?

nonLocalGuesses = guesses
}

def withFoundScaladocDef(result: DefinitionResult): DefinitionResult = {
fundScaladocDef = true
result
}

def build(): Option[Report] =
compilerDefn match {
case Some(compilerDefn) if !fundScaladocDef =>
Some(
Report(
"empty-definition",
s"""|empty definition using pc, found symbol in pc: ${compilerDefn.querySymbol}
|${semanticDBDefn match {
case None =>
s"semanticdb not found"
case Some(defn) if defn.isEmpty =>
s"empty definition using semanticdb"
case Some(defn) =>
s"found definition using semanticdb; symbol ${defn.symbol}"
}}
|${fallbackDefn.map {
case defn if defn.isEmpty =>
s"""|empty definition using fallback
|non-local guesses:
|${nonLocalGuesses.mkString("\t -", "\n\t -", "")}
|"""
case defn =>
s"found definition using fallback; symbol ${defn.symbol}"
}}
|Document text:
|
|```scala
|${Try(path.readText).toOption.getOrElse("Failed to read text")}
|```
|""".stripMargin,
s"empty definition using pc, found symbol in pc: ${compilerDefn.querySymbol}",
path = Some(path.toURI),
id = querySymbol.orElse(
Some(s"${path.toURI}:${params.getPosition().getLine()}")
),
error = error,
)
)
case _ => None
}

private def querySymbol: Option[String] =
compilerDefn.map(_.querySymbol) match {
case Some("") | None =>
semanticDBDefn
.map(_.querySymbol)
.orElse(fallbackDefn.map(_.querySymbol))
case res => res
}
}
Loading
Loading