Skip to content

Commit

Permalink
Backport "presentation-compiler: Add synthetic decorations" to LTS (#…
Browse files Browse the repository at this point in the history
…20759)

Backports #18951 to the LTS branch.

PR submitted by the release tooling.
[skip ci]
  • Loading branch information
WojciechMazur authored Jun 23, 2024
2 parents 785fea7 + 7a74114 commit ab667c5
Show file tree
Hide file tree
Showing 12 changed files with 902 additions and 72 deletions.
53 changes: 2 additions & 51 deletions presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ abstract class PcCollector[T](
val uri = params.uri().nn
val filePath = Paths.get(uri).nn
val sourceText = params.text().nn
val text = sourceText.toCharArray().nn
val source =
SourceFile.virtual(filePath.toString(), sourceText)
driver.run(uri, source)
Expand Down Expand Up @@ -70,45 +71,6 @@ abstract class PcCollector[T](
parent: Option[Tree]
)(tree: Tree| EndMarker, pos: SourcePosition, symbol: Option[Symbol]): T

/**
* @return (adjusted position, should strip backticks)
*/
def adjust(
pos1: SourcePosition,
forRename: Boolean = false
): (SourcePosition, Boolean) =
if !pos1.span.isCorrect then (pos1, false)
else
val pos0 =
val span = pos1.span
if span.exists && span.point > span.end then
pos1.withSpan(
span
.withStart(span.point)
.withEnd(span.point + (span.end - span.start))
)
else pos1

val pos =
if pos0.end > 0 && sourceText(pos0.end - 1) == ',' then
pos0.withEnd(pos0.end - 1)
else pos0
val isBackticked =
sourceText(pos.start) == '`' &&
pos.end > 0 &&
sourceText(pos.end - 1) == '`'
// when the old name contains backticks, the position is incorrect
val isOldNameBackticked = sourceText(pos.start) != '`' &&
pos.start > 0 &&
sourceText(pos.start - 1) == '`' &&
sourceText(pos.end) == '`'
if isBackticked && forRename then
(pos.withStart(pos.start + 1).withEnd(pos.end - 1), true)
else if isOldNameBackticked then
(pos.withStart(pos.start - 1).withEnd(pos.end + 1), false)
else (pos, false)
end adjust

def symbolAlternatives(sym: Symbol) =
def member(parent: Symbol) = parent.info.member(sym.name).symbol
def primaryConstructorTypeParam(owner: Symbol) =
Expand Down Expand Up @@ -447,7 +409,7 @@ abstract class PcCollector[T](
*/
case sel: Select
if sel.span.isCorrect && filter(sel) &&
!isForComprehensionMethod(sel) =>
!sel.isForComprehensionMethod =>
occurrences + collect(
sel,
pos.withSpan(selectNameSpan(sel))
Expand Down Expand Up @@ -602,17 +564,6 @@ abstract class PcCollector[T](
Span(span.start, span.start + realName.length, point)
else Span(point, span.end, point)
else span

private val forCompMethods =
Set(nme.map, nme.flatMap, nme.withFilter, nme.foreach)

// We don't want to collect synthethic `map`, `withFilter`, `foreach` and `flatMap` in for-comprenhensions
private def isForComprehensionMethod(sel: Select): Boolean =
val syntheticName = sel.name match
case name: TermName => forCompMethods(name)
case _ => false
val wrongSpan = sel.qualifier.span.contains(sel.nameSpan)
syntheticName && wrongSpan
end PcCollector

object PcCollector:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ final class PcDocumentHighlightProvider(
toAdjust: SourcePosition,
sym: Option[Symbol]
): DocumentHighlight =
val (pos, _) = adjust(toAdjust)
val (pos, _) = toAdjust.adjust(text)
tree match
case _: NamedDefTree =>
DocumentHighlight(pos.toLsp, DocumentHighlightKind.Write)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ final class PcInlineValueProviderImpl(
) extends PcCollector[Option[Occurence]](driver, params)
with InlineValueProvider:

val text = params.text().nn.toCharArray().nn

val position: l.Position = pos.toLsp.getStart().nn

override def collect(parent: Option[Tree])(
Expand All @@ -38,7 +36,7 @@ final class PcInlineValueProviderImpl(
): Option[Occurence] =
tree match
case tree: Tree =>
val (adjustedPos, _) = adjust(pos)
val (adjustedPos, _) = pos.adjust(text)
Some(Occurence(tree, parent, adjustedPos))
case _ => None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class PcRenameProvider(
def collect(
parent: Option[Tree]
)(tree: Tree | EndMarker, toAdjust: SourcePosition, sym: Option[Symbol]): l.TextEdit =
val (pos, stripBackticks) = adjust(toAdjust, forRename = true)
val (pos, stripBackticks) = toAdjust.adjust(text, forRename = true)
l.TextEdit(
pos.toLsp,
if stripBackticks then newName.stripBackticks else newName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ final class PcSemanticTokensProvider(
Some(
makeNode(
sym = sym,
pos = adjust(pos)._1,
pos = pos.adjust(text)._1,
isDefinition = isDefinition(tree),
isDeclaration = isDeclaration(tree)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package dotty.tools.pc


import java.nio.file.Paths

import scala.meta.internal.metals.ReportContext
import dotty.tools.pc.utils.MtagsEnrichments.*
import dotty.tools.pc.printer.ShortenedTypePrinter
import scala.meta.pc.SymbolSearch
import scala.meta.pc.SyntheticDecoration
import scala.meta.pc.SyntheticDecorationsParams
import scala.meta.internal.pc.DecorationKind
import scala.meta.internal.pc.Decoration


import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Flags
import dotty.tools.dotc.core.StdNames.*
import dotty.tools.dotc.core.Types.*
import dotty.tools.dotc.interactive.Interactive
import dotty.tools.dotc.interactive.InteractiveDriver
import dotty.tools.dotc.util.SourceFile
import dotty.tools.dotc.util.SourcePosition
import dotty.tools.dotc.util.Spans.Span
import dotty.tools.pc.IndexedContext

final class PcSyntheticDecorationsProvider(
driver: InteractiveDriver,
params: SyntheticDecorationsParams,
symbolSearch: SymbolSearch,
)(using ReportContext):

val uri = params.uri().nn
val filePath = Paths.get(uri).nn
val sourceText = params.text().nn
val text = sourceText.toCharArray().nn
val source =
SourceFile.virtual(filePath.toString, sourceText)
driver.run(uri, source)
given ctx: Context = driver.currentCtx
val unit = driver.currentCtx.run.nn.units.head

def tpdTree = unit.tpdTree

def provide(): List[SyntheticDecoration] =
val deepFolder = DeepFolder[Synthetics](collectDecorations)
deepFolder(Synthetics.empty, tpdTree).decorations

def collectDecorations(
decorations: Synthetics,
tree: Tree,
): Synthetics =
tree match
case ImplicitConversion(name, range) if params.implicitConversions() =>
val adjusted = range.adjust(text)._1
decorations
.add(
Decoration(
adjusted.startPos.toLsp,
name + "(",
DecorationKind.ImplicitConversion,
)
)
.add(
Decoration(
adjusted.endPos.toLsp,
")",
DecorationKind.ImplicitConversion,
)
)
case ImplicitParameters(names, pos, allImplicit)
if params.implicitParameters() =>
val label =
if allImplicit then names.mkString("(", ", ", ")")
else names.mkString(", ", ", ", "")
decorations.add(
Decoration(
pos.adjust(text)._1.toLsp,
label,
DecorationKind.ImplicitParameter,
)
)
case TypeParameters(tpes, pos, sel)
if params.typeParameters() && !syntheticTupleApply(sel) =>
val label = tpes.map(toLabel(_, pos)).mkString("[", ", ", "]")
decorations.add(
Decoration(
pos.adjust(text)._1.endPos.toLsp,
label,
DecorationKind.TypeParameter,
)
)
case InferredType(tpe, pos, defTree) if params.inferredTypes() =>
val adjustedPos = pos.adjust(text)._1.endPos
if decorations.containsDef(adjustedPos.start) then decorations
else
decorations.add(
Decoration(
adjustedPos.toLsp,
": " + toLabel(tpe, pos),
DecorationKind.InferredType,
),
adjustedPos.start,
)
case _ => decorations

private def toLabel(
tpe: Type,
pos: SourcePosition,
): String =
val tpdPath =
Interactive.pathTo(unit.tpdTree, pos.span)

val indexedCtx = IndexedContext(Interactive.contextOfPath(tpdPath))
val printer = ShortenedTypePrinter(
symbolSearch
)(using indexedCtx)
def optDealias(tpe: Type): Type =
def isInScope(tpe: Type): Boolean =
tpe match
case tref: TypeRef =>
indexedCtx.lookupSym(
tref.currentSymbol
) == IndexedContext.Result.InScope
case AppliedType(tycon, args) =>
isInScope(tycon) && args.forall(isInScope)
case _ => true
if isInScope(tpe)
then tpe
else tpe.metalsDealias(using indexedCtx.ctx)

val dealiased = optDealias(tpe)
printer.tpe(dealiased)
end toLabel

private val definitions = IndexedContext(ctx).ctx.definitions
private def syntheticTupleApply(tree: Tree): Boolean =
tree match
case sel: Select =>
if definitions.isTupleNType(sel.symbol.info.finalResultType) then
sel match
case Select(tupleClass: Ident, _)
if !tupleClass.span.isZeroExtent &&
tupleClass.span.exists &&
tupleClass.name.startsWith("Tuple") =>
val pos = tupleClass.sourcePos
!sourceText.slice(pos.start, pos.end).mkString.startsWith("Tuple")
case _ => true
else false
case _ => false
end PcSyntheticDecorationsProvider

object ImplicitConversion:
def unapply(tree: Tree)(using Context) =
tree match
case Apply(fun: Ident, args) if isSynthetic(fun) =>
implicitConversion(fun, args)
case Apply(Select(fun, name), args)
if name == nme.apply && isSynthetic(fun) =>
implicitConversion(fun, args)
case _ => None
private def isSynthetic(tree: Tree)(using Context) =
tree.span.isSynthetic && tree.symbol.isOneOf(Flags.GivenOrImplicit)

private def implicitConversion(fun: Tree, args: List[Tree])(using Context) =
val lastArgPos =
args.lastOption.map(_.sourcePos).getOrElse(fun.sourcePos)
Some(
fun.symbol.decodedName,
lastArgPos.withStart(fun.sourcePos.start),
)
end ImplicitConversion

object ImplicitParameters:
def unapply(tree: Tree)(using Context) =
tree match
case Apply(fun, args)
if args.exists(isSyntheticArg) && !tree.sourcePos.span.isZeroExtent =>
val (implicitArgs, providedArgs) = args.partition(isSyntheticArg)
val allImplicit = providedArgs.isEmpty
val pos = implicitArgs.head.sourcePos
Some(implicitArgs.map(_.symbol.decodedName), pos, allImplicit)
case Apply(ta @ TypeApply(fun, _), _)
if fun.span.isSynthetic && isValueOf(fun) =>
Some(
List("new " + tpnme.valueOf.decoded.capitalize + "(...)"),
fun.sourcePos,
true,
)
case _ => None
private def isValueOf(tree: Tree)(using Context) =
val symbol = tree.symbol.maybeOwner
symbol.name.decoded == tpnme.valueOf.decoded.capitalize
private def isSyntheticArg(tree: Tree)(using Context) = tree match
case tree: Ident =>
tree.span.isSynthetic && tree.symbol.isOneOf(Flags.GivenOrImplicit)
case _ => false
end ImplicitParameters

object TypeParameters:
def unapply(tree: Tree)(using Context) =
tree match
case TypeApply(sel: Select, _) if sel.isForComprehensionMethod => None
case TypeApply(fun, args) if inferredTypeArgs(args) =>
val pos = fun match
case sel: Select if sel.isInfix =>
sel.sourcePos.withEnd(sel.nameSpan.end)
case _ => fun.sourcePos
val tpes = args.map(_.tpe.stripTypeVar.widen.finalResultType)
Some((tpes, pos.endPos, fun))
case _ => None
private def inferredTypeArgs(args: List[Tree]): Boolean =
args.forall {
case tt: TypeTree if tt.span.exists && !tt.span.isZeroExtent => true
case _ => false
}
end TypeParameters

object InferredType:
def unapply(tree: Tree)(using Context) =
tree match
case vd @ ValDef(_, tpe, _)
if isValidSpan(tpe.span, vd.nameSpan) &&
!vd.symbol.is(Flags.Enum) =>
if vd.symbol == vd.symbol.sourceSymbol then
Some(tpe.tpe, tpe.sourcePos.withSpan(vd.nameSpan), vd)
else None
case vd @ DefDef(_, _, tpe, _)
if isValidSpan(tpe.span, vd.nameSpan) &&
tpe.span.start >= vd.nameSpan.end &&
!vd.symbol.isConstructor &&
!vd.symbol.is(Flags.Mutable) =>
if vd.symbol == vd.symbol.sourceSymbol then
Some(tpe.tpe, tpe.sourcePos, vd)
else None
case bd @ Bind(
name,
Ident(nme.WILDCARD),
) =>
Some(bd.symbol.info, bd.namePos, bd)
case _ => None

private def isValidSpan(tpeSpan: Span, nameSpan: Span): Boolean =
tpeSpan.isZeroExtent &&
nameSpan.exists &&
!nameSpan.isZeroExtent

end InferredType

case class Synthetics(
decorations: List[Decoration],
definitions: Set[Int],
):
def containsDef(offset: Int) = definitions(offset)
def add(decoration: Decoration, offset: Int) =
copy(
decorations = decoration :: decorations,
definitions = definitions + offset,
)
def add(decoration: Decoration) =
copy(decorations = decoration :: decorations)

object Synthetics:
def empty: Synthetics = Synthetics(Nil, Set.empty)
Loading

0 comments on commit ab667c5

Please sign in to comment.