Skip to content

Commit 6a1714b

Browse files
authored
fix: hover and go to definition for named tuples (#22202)
resolves: #20500
1 parent 019d203 commit 6a1714b

File tree

5 files changed

+113
-33
lines changed

5 files changed

+113
-33
lines changed

Diff for: presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala

+37-14
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ object HoverProvider:
104104
) match
105105
case Nil =>
106106
fallbackToDynamics(path, printer, contentType)
107-
case (symbol, tpe) :: _
107+
case (symbol, tpe, _) :: _
108108
if symbol.name == nme.selectDynamic || symbol.name == nme.applyDynamic =>
109109
fallbackToDynamics(path, printer, contentType)
110-
case symbolTpes @ ((symbol, tpe) :: _) =>
110+
case symbolTpes @ ((symbol, tpe, None) :: _) =>
111111
val exprTpw = tpe.widenTermRefExpr.deepDealias
112112
val hoverString =
113113
tpw match
@@ -153,6 +153,21 @@ object HoverProvider:
153153
case _ =>
154154
ju.Optional.empty().nn
155155
end match
156+
case (_, tpe, Some(namedTupleArg)) :: _ =>
157+
val exprTpw = tpe.widenTermRefExpr.deepDealias
158+
printer.expressionType(exprTpw) match
159+
case Some(tpe) =>
160+
ju.Optional.of(
161+
new ScalaHover(
162+
expressionType = Some(tpe),
163+
symbolSignature = Some(s"$namedTupleArg: $tpe"),
164+
docstring = None,
165+
forceExpressionType = false,
166+
contextInfo = printer.getUsedRenamesInfo,
167+
contentType = contentType
168+
)
169+
).nn
170+
case _ => ju.Optional.empty().nn
156171
end match
157172
end if
158173
end hover
@@ -165,23 +180,31 @@ object HoverProvider:
165180
printer: ShortenedTypePrinter,
166181
contentType: ContentType
167182
)(using Context): ju.Optional[HoverSignature] = path match
168-
case SelectDynamicExtractor(sel, n, name) =>
183+
case SelectDynamicExtractor(sel, n, name, rest) =>
169184
def findRefinement(tp: Type): Option[HoverSignature] =
170185
tp match
171-
case RefinedType(_, refName, tpe) if name == refName.toString() =>
186+
case RefinedType(_, refName, tpe) if (name == refName.toString() || refName.toString() == nme.Fields.toString()) =>
187+
val resultType =
188+
rest match
189+
case Select(_, asInstanceOf) :: TypeApply(_, List(tpe)) :: _ if asInstanceOf == nme.asInstanceOfPM =>
190+
tpe.tpe.widenTermRefExpr.deepDealias
191+
case _ if n == nme.selectDynamic => tpe.resultType
192+
case _ => tpe
193+
172194
val tpeString =
173-
if n == nme.selectDynamic then s": ${printer.tpe(tpe.resultType)}"
174-
else printer.tpe(tpe)
195+
if n == nme.selectDynamic then s": ${printer.tpe(resultType)}"
196+
else printer.tpe(resultType)
175197

176198
val valOrDef =
177-
if n == nme.selectDynamic && !tpe.isInstanceOf[ExprType]
178-
then "val"
179-
else "def"
199+
if refName.toString() == nme.Fields.toString() then ""
200+
else if n == nme.selectDynamic && !tpe.isInstanceOf[ExprType]
201+
then "val "
202+
else "def "
180203

181204
Some(
182205
new ScalaHover(
183206
expressionType = Some(tpeString),
184-
symbolSignature = Some(s"$valOrDef $name$tpeString"),
207+
symbolSignature = Some(s"$valOrDef$name$tpeString"),
185208
contextInfo = printer.getUsedRenamesInfo,
186209
contentType = contentType
187210
)
@@ -208,16 +231,16 @@ object SelectDynamicExtractor:
208231
case Select(_, _) :: Apply(
209232
Select(Apply(reflSel, List(sel)), n),
210233
List(Literal(Constant(name: String)))
211-
) :: _
234+
) :: rest
212235
if (n == nme.selectDynamic || n == nme.applyDynamic) &&
213236
nme.reflectiveSelectable == reflSel.symbol.name =>
214-
Some(sel, n, name)
237+
Some(sel, n, name, rest)
215238
// tests `selectable`, `selectable2` and `selectable-full` in HoverScala3TypeSuite
216239
case Select(_, _) :: Apply(
217240
Select(sel, n),
218241
List(Literal(Constant(name: String)))
219-
) :: _ if n == nme.selectDynamic || n == nme.applyDynamic =>
220-
Some(sel, n, name)
242+
) :: rest if n == nme.selectDynamic || n == nme.applyDynamic =>
243+
Some(sel, n, name, rest)
221244
case _ => None
222245
end match
223246
end unapply

Diff for: presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala

+36-18
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import scala.annotation.tailrec
55

66
import dotc.*
77
import ast.*, tpd.*
8+
import dotty.tools.dotc.core.Constants.*
89
import core.*, Contexts.*, Flags.*, Names.*, Symbols.*, Types.*
10+
import dotty.tools.dotc.core.StdNames.*
911
import interactive.*
1012
import util.*
1113
import util.SourcePosition
14+
import dotty.tools.pc.utils.InteractiveEnrichments.*
1215

1316
object MetalsInteractive:
17+
type NamedTupleArg = String
1418

1519
def contextOfStat(
1620
stats: List[Tree],
@@ -110,67 +114,67 @@ object MetalsInteractive:
110114
pos: SourcePosition,
111115
indexed: IndexedContext,
112116
skipCheckOnName: Boolean = false
113-
): List[(Symbol, Type)] =
117+
): List[(Symbol, Type, Option[String])] =
114118
import indexed.ctx
115119
path match
116120
// For a named arg, find the target `DefDef` and jump to the param
117121
case NamedArg(name, _) :: Apply(fn, _) :: _ =>
118122
val funSym = fn.symbol
119123
if funSym.is(Synthetic) && funSym.owner.is(CaseClass) then
120124
val sym = funSym.owner.info.member(name).symbol
121-
List((sym, sym.info))
125+
List((sym, sym.info, None))
122126
else
123127
val paramSymbol =
124128
for param <- funSym.paramSymss.flatten.find(_.name == name)
125129
yield param
126130
val sym = paramSymbol.getOrElse(fn.symbol)
127-
List((sym, sym.info))
131+
List((sym, sym.info, None))
128132

129133
case (_: untpd.ImportSelector) :: (imp: Import) :: _ =>
130134
importedSymbols(imp, _.span.contains(pos.span)).map(sym =>
131-
(sym, sym.info)
135+
(sym, sym.info, None)
132136
)
133137

134138
case (imp: Import) :: _ =>
135139
importedSymbols(imp, _.span.contains(pos.span)).map(sym =>
136-
(sym, sym.info)
140+
(sym, sym.info, None)
137141
)
138142

139143
// wildcard param
140144
case head :: _ if (head.symbol.is(Param) && head.symbol.is(Synthetic)) =>
141-
List((head.symbol, head.typeOpt))
145+
List((head.symbol, head.typeOpt, None))
142146

143147
case (head @ Select(target, name)) :: _
144148
if head.symbol.is(Synthetic) && name == StdNames.nme.apply =>
145149
val sym = target.symbol
146150
if sym.is(Synthetic) && sym.is(Module) then
147-
List((sym.companionClass, sym.companionClass.info))
148-
else List((target.symbol, target.typeOpt))
151+
List((sym.companionClass, sym.companionClass.info, None))
152+
else List((target.symbol, target.typeOpt, None))
149153

150154
// L@@ft(...)
151155
case (head @ ApplySelect(select)) :: _
152156
if select.qualifier.sourcePos.contains(pos) &&
153157
select.name == StdNames.nme.apply =>
154-
List((head.symbol, head.typeOpt))
158+
List((head.symbol, head.typeOpt, None))
155159

156160
// for Inlined we don't have a symbol, but it's needed to show proper type
157161
case (head @ Inlined(call, bindings, expansion)) :: _ =>
158-
List((call.symbol, head.typeOpt))
162+
List((call.symbol, head.typeOpt, None))
159163

160164
// for comprehension
161165
case (head @ ApplySelect(select)) :: _ if isForSynthetic(head) =>
162166
// If the cursor is on the qualifier, return the symbol for it
163167
// `for { x <- List(1).head@@Option }` returns the symbol of `headOption`
164168
if select.qualifier.sourcePos.contains(pos) then
165-
List((select.qualifier.symbol, select.qualifier.typeOpt))
169+
List((select.qualifier.symbol, select.qualifier.typeOpt, None))
166170
// Otherwise, returns the symbol of for synthetics such as "withFilter"
167-
else List((head.symbol, head.typeOpt))
171+
else List((head.symbol, head.typeOpt, None))
168172

169173
// f@@oo.bar
170174
case Select(target, _) :: _
171175
if target.span.isSourceDerived &&
172176
target.sourcePos.contains(pos) =>
173-
List((target.symbol, target.typeOpt))
177+
List((target.symbol, target.typeOpt, None))
174178

175179
/* In some cases type might be represented by TypeTree, however it's possible
176180
* that the type tree will not be marked properly as synthetic even if it doesn't
@@ -185,7 +189,7 @@ object MetalsInteractive:
185189
*/
186190
case (tpt: TypeTree) :: parent :: _
187191
if tpt.span != parent.span && !tpt.symbol.is(Synthetic) =>
188-
List((tpt.symbol, tpt.typeOpt))
192+
List((tpt.symbol, tpt.typeOpt, None))
189193

190194
/* TypeTest class https://dotty.epfl.ch/docs/reference/other-new-features/type-test.html
191195
* compiler automatically adds unapply if possible, we need to find the type symbol
@@ -195,14 +199,28 @@ object MetalsInteractive:
195199
pat match
196200
case UnApply(fun, _, pats) =>
197201
val tpeSym = pats.head.typeOpt.typeSymbol
198-
List((tpeSym, tpeSym.info))
202+
List((tpeSym, tpeSym.info, None))
199203
case _ =>
200204
Nil
201205

206+
// Handle select on named tuples
207+
case (Apply(Apply(TypeApply(fun, List(t1, t2)), List(ddef)), List(Literal(Constant(i: Int))))) :: _
208+
if fun.symbol.exists && fun.symbol.name == nme.apply &&
209+
fun.symbol.owner.exists && fun.symbol.owner == getModuleIfDefined("scala.NamedTuple").moduleClass =>
210+
def getIndex(t: Tree): Option[Type] =
211+
t.tpe.dealias match
212+
case AppliedType(_, args) => args.get(i)
213+
case _ => None
214+
val name = getIndex(t1) match
215+
case Some(c: ConstantType) => c.value.stringValue
216+
case _ => ""
217+
val tpe = getIndex(t2).getOrElse(NoType)
218+
List((ddef.symbol, tpe, Some(name)))
219+
202220
case path @ head :: tail =>
203221
if head.symbol.is(Exported) then
204222
val sym = head.symbol.sourceSymbol
205-
List((sym, sym.info))
223+
List((sym, sym.info, None))
206224
else if head.symbol.is(Synthetic) then
207225
enclosingSymbolsWithExpressionType(
208226
tail,
@@ -217,7 +235,7 @@ object MetalsInteractive:
217235
pos,
218236
indexed.ctx.source
219237
)
220-
then List((head.symbol, head.typeOpt))
238+
then List((head.symbol, head.typeOpt, None))
221239
/* Type tree for List(1) has an Int type variable, which has span
222240
* but doesn't exist in code.
223241
* https://github.com/scala/scala3/issues/15937
@@ -234,7 +252,7 @@ object MetalsInteractive:
234252
indexed,
235253
skipCheckOnName
236254
)
237-
else recovered.map(sym => (sym, sym.info))
255+
else recovered.map(sym => (sym, sym.info, None))
238256
end if
239257
case Nil => Nil
240258
end match

Diff for: presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class PcDefinitionProvider(
101101
val enclosing = path.expandRangeToEnclosingApply(pos)
102102
val typeSymbols = MetalsInteractive
103103
.enclosingSymbolsWithExpressionType(enclosing, pos, indexed)
104-
.map { case (_, tpe) =>
104+
.map { case (_, tpe, _) =>
105105
tpe.typeSymbol
106106
}
107107
typeSymbols match

Diff for: presentation-compiler/test/dotty/tools/pc/tests/definition/PcDefinitionSuite.scala

+9
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,12 @@ class PcDefinitionSuite extends BasePcDefinitionSuite:
504504
|val a = MyIntOut(1).un@@even
505505
|""".stripMargin,
506506
)
507+
508+
@Test def `named-tuples` =
509+
check(
510+
"""|import scala.language.experimental.namedTuples
511+
|
512+
|val <<foo>> = (name = "Bob", age = 42, height = 1.9d)
513+
|val foo_name = foo.na@@me
514+
|""".stripMargin
515+
)

Diff for: presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTermSuite.scala

+30
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,33 @@ class HoverTermSuite extends BaseHoverSuite:
717717
|""".stripMargin,
718718
"""def ???: Nothing""".stripMargin.hover
719719
)
720+
721+
@Test def `named-tuples`: Unit =
722+
check(
723+
"""import scala.language.experimental.namedTuples
724+
|
725+
|val foo = (name = "Bob", age = 42, height = 1.9d)
726+
|val foo_name = foo.na@@me
727+
|""".stripMargin,
728+
"name: String".hover
729+
)
730+
731+
@Test def `named-tuples2`: Unit =
732+
check(
733+
"""|import scala.language.experimental.namedTuples
734+
|
735+
|import NamedTuple.*
736+
|
737+
|class NamedTupleSelectable extends Selectable {
738+
| type Fields <: AnyNamedTuple
739+
| def selectDynamic(name: String): Any = ???
740+
|}
741+
|
742+
|val person = new NamedTupleSelectable {
743+
| type Fields = (name: String, city: String)
744+
|}
745+
|
746+
|val person_name = person.na@@me
747+
|""".stripMargin,
748+
"name: String".hover
749+
)

0 commit comments

Comments
 (0)