Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
@@ -0,0 +1,76 @@
package scala.meta.internal.metals.codeactions

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import scala.meta.internal.jpc.JavacDiagnostic
import scala.meta.internal.metals.Compilers
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.io.AbsolutePath
import scala.meta.pc.CancelToken
import scala.meta.pc.CodeActionId

import org.eclipse.{lsp4j => l}

class ChangeVariableType(
compilers: Compilers
) extends CodeAction {

override def kind: String = l.CodeActionKind.QuickFix
override def isScala: Boolean = false
override def isJava: Boolean = true

override def contribute(
params: l.CodeActionParams,
token: CancelToken,
)(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]] = {
val path = params.getTextDocument().getUri().toAbsolutePath
val position = params.getRange().getStart()

val matchingDiagnostics =
params.getContext().getDiagnostics().asScala.toSeq.collect {
case diagnostic
if JavacDiagnostic.IncompatibleTypes
.unapply(diagnostic)
.isDefined =>
diagnostic
}

val editParams =
new l.TextDocumentPositionParams(params.getTextDocument(), position)
Future
.sequence {
matchingDiagnostics.map { diagnostic =>
compilers
.codeAction(
editParams,
token,
CodeActionId.ChangeVariableType,
Some(diagnostic.getRange()),
)
.map { edits =>
if (edits.isEmpty()) None
else Some(build(path, diagnostic, edits))
}
}
}
.map(_.flatten)
}

private def build(
path: AbsolutePath,
diagnostic: l.Diagnostic,
edits: java.util.List[l.TextEdit],
): l.CodeAction = {
CodeActionBuilder.build(
ChangeVariableType.title,
kind,
diagnostics = List(diagnostic),
changes = Seq(path -> edits.asScala.toSeq),
)
}
}

object ChangeVariableType {
val title = "Change variable type to match assigned value"
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ final class CodeActionProvider(
new GenerateGettersSetters(javaTrees, buffers),
new GenerateEqualsHashCodeToString(javaTrees, buffers),
new AddMissingReturnStatement(javaTrees, buffers),
new ChangeVariableType(compilers),
new JavaExtractMethodCodeAction(javaTrees, compilers),
)

Expand Down
66 changes: 58 additions & 8 deletions metals/src/main/scala/scala/meta/internal/parsing/JavaTrees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -243,28 +243,76 @@ class JavaTrees(buffers: Buffers) {
protected def javaVariable(node: VariableTree): Option[JavaVariable] = {
val variableName = node.getName().toString()
treeRange(node).map { range =>
val nameRange = findNameRange(
lineMap,
text,
range.startOffset,
range.endOffset,
variableName,
).getOrElse(range)
val typeTree = Option(node.getType())
JavaVariable(
tree = node,
name = variableName,
range = range,
nameRange = findNameRange(
lineMap,
text,
range.startOffset,
range.endOffset,
variableName,
).getOrElse(range),
typ = Option(node.getType()) match {
nameRange = nameRange,
typ = typeTree match {
case Some(t) => t.toString()
case None =>
// This can happen if a variable is declared with inferred type.
// This is going to change in Java 27, see https://bugs.openjdk.org/browse/JDK-8268850
"var"
},
typeRange = typeTree.flatMap(typeRange(_, nameRange)),
initializerRange = Option(node.getInitializer()).flatMap(treeRange),
modifiers = node.getModifiers().getFlags().asScala.toSet,
)
}
}

private val LegacyArrayDimensions = """\s*(?:\[\s*\]\s*)+""".r

private def typeRange(
typeTree: Tree,
nameRange: JavaRange,
): Option[JavaRange] =
treeRange(typeTree).flatMap { range =>
val legacyArraySuffix =
range.endOffset > nameRange.endOffset &&
onlyLegacyArrayDimensions(
text.substring(nameRange.endOffset, range.endOffset)
)
if (legacyArraySuffix) {
val endOffset = lastNonWhitespaceBefore(nameRange.startOffset)
if (endOffset <= range.startOffset) None
else {
val end = endOffset + 1
Some(
range.copy(
range = Positions.toLspRange(
lineMap,
range.startOffset,
end,
text,
),
endOffset = end,
)
)
}
} else Some(range)
}

private def onlyLegacyArrayDimensions(suffix: String): Boolean =
suffix match {
case LegacyArrayDimensions() => true
case _ => false
}

private def lastNonWhitespaceBefore(offset: Int): Int = {
var i = offset - 1
while (i >= 0 && text.charAt(i).isWhitespace) i -= 1
i
}
}

private class EnclosingMethodFinder(
Expand Down Expand Up @@ -772,6 +820,8 @@ case class JavaVariable(
range: JavaRange,
nameRange: JavaRange,
typ: String,
typeRange: Option[JavaRange],
initializerRange: Option[JavaRange],
modifiers: Set[Modifier],
) extends JavaMember
with HasModifiers {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
public class CodeActionId {
public static final String ConvertToNamedArguments = "ConvertToNamedArguments";
public static final String ChangeVariableType = "ChangeVariableType";
public static final String ExtractMethod = "ExtractMethod";
public static final String ImplementAbstractMembers = "ImplementAbstractMembers";
public static final String ImportMissingSymbol = "ImportMissingSymbol";
Expand Down
Loading
Loading