diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala index 34e01968e64..06937e06caa 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala @@ -156,8 +156,11 @@ final class BuildTargets private ( def javaTarget(id: BuildTargetIdentifier): Option[JavaTarget] = data.fromOptions(_.javaTarget(id)) - def jvmTarget(id: BuildTargetIdentifier): Option[JvmTarget] = - data.fromOptions(_.jvmTarget(id)) + def jvmTarget( + id: BuildTargetIdentifier, + scalaPreferred: Boolean = true, + ): Option[JvmTarget] = + data.fromOptions(_.jvmTarget(id, scalaPreferred)) def fullClasspath( id: BuildTargetIdentifier, diff --git a/metals/src/main/scala/scala/meta/internal/metals/CompilerConfiguration.scala b/metals/src/main/scala/scala/meta/internal/metals/CompilerConfiguration.scala index af677c20db4..55398c8bafc 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/CompilerConfiguration.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/CompilerConfiguration.scala @@ -453,9 +453,10 @@ class CompilerConfiguration( val pc = JavaPresentationCompiler() val shouldUseOpts = featureFlags .readBoolean(FeatureFlag.JAVAC_OPTIONS) - .orElse(false) + .orElse(true) val options = javaTarget match { - case j: JavaTarget if shouldUseOpts => j.options + case j: JavaTarget if shouldUseOpts => + CompilerConfiguration.filterJavaPcOptions(j.options) case _ => Nil } configure(pc, search, completionItemPriority) @@ -678,3 +679,17 @@ class CompilerConfiguration( Nil } } + +object CompilerConfiguration { + private val excludedJavaPcOptionPrefixes = List( + "-J", + "@", + "-Xplugin:", + "-proc:", + ) + + private[metals] def filterJavaPcOptions(options: List[String]): List[String] = + options.filterNot(option => + excludedJavaPcOptionPrefixes.exists(option.startsWith) + ) +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala index a18e7b62a05..29c6d39817d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala @@ -1795,7 +1795,7 @@ class Compilers( private def loadJavaCompiler( targetId: BuildTargetIdentifier ): Option[PresentationCompiler] = { - buildTargets.jvmTarget(targetId).map { javaTarget => + buildTargets.jvmTarget(targetId, scalaPreferred = false).map { javaTarget => jcache .computeIfAbsent( PresentationCompilerKey.JavaBuildTarget(targetId), diff --git a/metals/src/main/scala/scala/meta/internal/metals/ModuleStatus.scala b/metals/src/main/scala/scala/meta/internal/metals/ModuleStatus.scala index c440df855ad..fe793c67334 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ModuleStatus.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ModuleStatus.scala @@ -54,7 +54,7 @@ class ModuleStatus( case Some(buildTarget) => handler.diagnostics .upstreamTargetsWithCompilationErrors(buildTarget.id) - .flatMap(handler.buildTargets.jvmTarget) + .flatMap(handler.buildTargets.jvmTarget(_)) .headOption match { case Some(buildTargetWithError) => client.metalsStatus( diff --git a/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala b/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala index 0fa213b26f0..763d4aaea30 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala @@ -147,8 +147,12 @@ final class TargetData() { scalaTargetInfo.get(id) def javaTarget(id: BuildTargetIdentifier): Option[JavaTarget] = javaTargetInfo.get(id) - def jvmTarget(id: BuildTargetIdentifier): Option[JvmTarget] = - scalaTarget(id).orElse(javaTarget(id)) + def jvmTarget( + id: BuildTargetIdentifier, + scalaPreferred: Boolean = true, + ): Option[JvmTarget] = + if (scalaPreferred) scalaTarget(id).orElse(javaTarget(id)) + else javaTarget(id).orElse(scalaTarget(id)) def jvmTargets(id: BuildTargetIdentifier): List[JvmTarget] = List(scalaTarget(id), javaTarget(id)).flatten diff --git a/metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala index ff8b67ed298..e7dff7b9cec 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala @@ -63,6 +63,8 @@ final class CodeActionProvider( new ConvertToNamedLambdaParameters(trees, compilers), new AddMissingOverrideAnnotation(javaTrees, buffers), new RemoveUnusedJavaImport(buffers), + new RemoveRedundantCast(javaTrees, buffers), + new SuppressWarnings(javaTrees, buffers), new GenerateConstructors(javaTrees, buffers), new GenerateGettersSetters(javaTrees, buffers), new GenerateEqualsHashCodeToString(javaTrees, buffers), diff --git a/metals/src/main/scala/scala/meta/internal/metals/codeactions/RemoveRedundantCast.scala b/metals/src/main/scala/scala/meta/internal/metals/codeactions/RemoveRedundantCast.scala new file mode 100644 index 00000000000..3026482b920 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/codeactions/RemoveRedundantCast.scala @@ -0,0 +1,84 @@ +package scala.meta.internal.metals.codeactions + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import scala.meta.internal.metals.Buffers +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.parsing.JavaTrees +import scala.meta.internal.parsing.JavaTypeCast +import scala.meta.pc.CancelToken + +import org.eclipse.{lsp4j => l} + +class RemoveRedundantCast(javaTrees: JavaTrees, buffers: Buffers) + extends CodeAction { + import RemoveRedundantCast._ + + 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]] = Future { + val path = params.getTextDocument().getUri().toAbsolutePath + val range = params.getRange() + + for { + text <- buffers.get(path).orElse(path.readTextOpt).toSeq + diagnostic <- params.getContext().getDiagnostics().asScala.toSeq + if isRedundantCast(diagnostic) + if range.overlapsWith(diagnostic.getRange()) + cast <- javaTrees + .findTypeCast(path, diagnostic.getRange().getStart()) + .toSeq + } yield CodeActionBuilder.build( + title, + kind, + diagnostics = List(diagnostic), + changes = Seq(path -> Seq(removeCastEdit(text, cast))), + ) + } +} + +object RemoveRedundantCast { + val title = "Remove redundant cast" + + private val RedundantCastCode = "compiler.warn.redundant.cast" + + private def isRedundantCast(diagnostic: l.Diagnostic): Boolean = + Option(diagnostic.getCode()).exists(code => + code.isLeft() && code.getLeft() == RedundantCastCode + ) + + private def removeCastEdit(text: String, cast: JavaTypeCast): l.TextEdit = { + val castStart = cast.typeRange.startOffset + val typeEnd = cast.typeRange.endOffset + val editEnd = typeEnd + text + .substring(typeEnd, cast.exprRange.startOffset) + .takeWhile(ch => ch == ' ' || ch == '\t') + .length + val editStart = + if ( + editEnd >= text.length || text.charAt(editEnd) == '\n' || text + .charAt(editEnd) == '\r' + ) + castStart - JavaMemberInsertion + .linePrefix(text, castStart) + .reverse + .takeWhile(ch => ch == ' ' || ch == '\t') + .length + else + castStart + new l.TextEdit( + new l.Range( + text.indexToLspPosition(editStart), + text.indexToLspPosition(editEnd), + ), + "", + ) + } + +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/codeactions/SuppressWarnings.scala b/metals/src/main/scala/scala/meta/internal/metals/codeactions/SuppressWarnings.scala new file mode 100644 index 00000000000..a3f2aa47a57 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/codeactions/SuppressWarnings.scala @@ -0,0 +1,226 @@ +package scala.meta.internal.metals.codeactions + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import scala.meta.internal.metals.Buffers +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.parsing.JavaAnnotation +import scala.meta.internal.parsing.JavaMember +import scala.meta.internal.parsing.JavaRange +import scala.meta.internal.parsing.JavaTrees +import scala.meta.io.AbsolutePath +import scala.meta.pc.CancelToken + +import com.google.gson.JsonPrimitive +import org.eclipse.{lsp4j => l} + +class SuppressWarnings( + javaTrees: JavaTrees, + buffers: Buffers, +) extends CodeAction { + import SuppressWarnings._ + + 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]] = Future { + val path = params.getTextDocument().getUri().toAbsolutePath + val range = params.getRange() + + val actions = for { + text <- buffers.get(path).orElse(path.readTextOpt).toSeq + diagnostic <- params.getContext().getDiagnostics().asScala.toSeq + warningName <- warningName(diagnostic).toSeq + if range.overlapsWith(diagnostic.getRange()) || + isZeroRange(diagnostic.getRange()) + position = + if (range.overlapsWith(diagnostic.getRange())) + diagnostic.getRange().getStart() + else range.getStart() + member <- enclosingMember(path, position).toSeq + edit <- suppressEdit(text, path, member, warningName).toSeq + } yield CodeActionBuilder.build( + title(warningName), + kind, + diagnostics = List(diagnostic), + changes = Seq(path -> Seq(edit)), + ) + actions.distinctBy(_.getEdit()) + } + + private def enclosingMember( + path: AbsolutePath, + position: l.Position, + ): Option[SuppressTarget] = + javaTrees + .findEnclosingJavaVariable(path, position, onNameOnly = false) + .filter(variable => + variable.isStandaloneDeclaration && + position <= variable.nameRange.getEnd() + ) + .map(variable => SuppressTarget(variable, variable.nameRange)) + .orElse( + javaTrees + .findEnclosingJavaMethod(path, position) + .map(method => SuppressTarget(method, method.nameRange)) + ) + .orElse( + javaTrees + .findEnclosingJavaClass(path, position) + .map(cls => SuppressTarget(cls, cls.nameRange)) + ) + + private def suppressEdit( + text: String, + path: scala.meta.io.AbsolutePath, + target: SuppressTarget, + warningName: String, + ): Option[l.TextEdit] = { + val annotations = javaTrees.memberAnnotations(path, target.member) + existingSuppressWarnings(annotations) match { + case Some(existing) => appendWarningEdit(text, existing, warningName) + case None => + Some(insertSuppressWarningsEdit(text, target, warningName, annotations)) + } + } + + private def insertSuppressWarningsEdit( + text: String, + target: SuppressTarget, + warningName: String, + annotations: List[JavaAnnotation], + ): l.TextEdit = { + val declarationOffset = declarationStartOffset(text, target, annotations) + val declarationStart = text.indexToLspPosition(declarationOffset) + val linePrefix = + JavaMemberInsertion.linePrefix(text, declarationOffset) + val (position, newText) = + if (linePrefix.forall(_.isWhitespace)) + ( + new l.Position(declarationStart.getLine(), 0), + s"""$linePrefix@SuppressWarnings("$warningName") + |""".stripMargin, + ) + else (declarationStart, s"""@SuppressWarnings("$warningName") """) + + new l.TextEdit(new l.Range(position, position), newText) + } + + private def declarationStartOffset( + text: String, + target: SuppressTarget, + annotations: List[JavaAnnotation], + ): Int = { + val afterAnnotations = annotations + .maxByOption(_.range.endOffset) + .map(_.range.endOffset) + .getOrElse(target.member.range.startOffset) + var offset = afterAnnotations + while ( + offset < target.nameRange.startOffset && text.charAt(offset).isWhitespace + ) + offset += 1 + offset + } +} + +object SuppressWarnings { + def title(warningName: String): String = + s"""Add @SuppressWarnings("$warningName")""" + + private def warningName(diagnostic: l.Diagnostic): Option[String] = + Option + .when(diagnostic.getSource() == "javac")(diagnostic.getData()) + .flatMap { + case value: String => Some(value) + case value: JsonPrimitive if value.isString() => + Some(value.getAsString()) + case _ => None + } + + private def isZeroRange(range: l.Range): Boolean = + range.isOffset && + range.getStart().getLine() == 0 && + range.getStart().getCharacter() == 0 + + private def existingSuppressWarnings( + annotations: List[JavaAnnotation] + ): Option[ExistingSuppressWarnings] = + annotations + .collectFirst { + case ann + if ann.name == "SuppressWarnings" || + ann.name.endsWith(".SuppressWarnings") => + ann.argsRange + } + .flatten + .map { case (open, close) => ExistingSuppressWarnings(open, close) } + + private def appendWarningEdit( + text: String, + existing: ExistingSuppressWarnings, + warningName: String, + ): Option[l.TextEdit] = { + val insideStart = existing.openParenOffset + 1 + val insideEnd = existing.closeParenOffset + val inside = text.substring(insideStart, insideEnd) + if (inside.contains(s""""$warningName"""")) None + else { + val trimmed = inside.trim() + val (namedValuePrefix, value) = trimmed match { + case NamedValueArgument(prefix, value) => (prefix, value.trim()) + case _ => ("", trimmed) + } + val isArray = value.startsWith("{") && value.endsWith("}") + val arrayContents = + if (isArray) value.substring(1, value.length() - 1).trim() + else "" + val (range, newText) = + if (isArray && arrayContents.isEmpty()) { + ( + new l.Range( + text.indexToLspPosition(insideStart), + text.indexToLspPosition(insideEnd), + ), + s"""$namedValuePrefix{"$warningName"}""", + ) + } else if (isArray) { + val closeBrace = insideStart + inside.lastIndexOf('}') + val separator = if (arrayContents.endsWith(",")) " " else ", " + ( + new l.Range( + text.indexToLspPosition(closeBrace), + text.indexToLspPosition(closeBrace), + ), + s"""$separator"$warningName"""", + ) + } else { + ( + new l.Range( + text.indexToLspPosition(insideStart), + text.indexToLspPosition(insideEnd), + ), + s"""$namedValuePrefix{$value, "$warningName"}""", + ) + } + Some(new l.TextEdit(range, newText)) + } + } + + private val NamedValueArgument = """(?s)(value\s*=\s*)(.*)""".r + + private case class SuppressTarget( + member: JavaMember, + nameRange: JavaRange, + ) + + private case class ExistingSuppressWarnings( + openParenOffset: Int, + closeParenOffset: Int, + ) +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/debug/server/DebugeeParamsCreator.scala b/metals/src/main/scala/scala/meta/internal/metals/debug/server/DebugeeParamsCreator.scala index 71c9696914e..bc2e8bbc095 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/debug/server/DebugeeParamsCreator.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/debug/server/DebugeeParamsCreator.scala @@ -56,7 +56,7 @@ class DebugeeParamsCreator(buildTargetClasses: BuildTargetClasses) { val modules = buildTargets .buildTargetTransitiveDependencies(id) - .flatMap(buildTargets.jvmTarget) + .flatMap(buildTargets.jvmTarget(_)) .map(createModule(_)) .toSeq diff --git a/metals/src/main/scala/scala/meta/internal/metals/mcp/MetalsMcpTools.scala b/metals/src/main/scala/scala/meta/internal/metals/mcp/MetalsMcpTools.scala index a0a5367fc38..14c6cab89da 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/mcp/MetalsMcpTools.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/mcp/MetalsMcpTools.scala @@ -378,7 +378,7 @@ trait MetalsMcpTools extends Cancelable { .upstreamTargetsWithCompilationErrors(buildTarget) if (upstreamModules.nonEmpty) { val modules = upstreamModules - .flatMap(buildTargets.jvmTarget) + .flatMap(buildTargets.jvmTarget(_)) .map(_.displayName) .mkString("\n", "\n", "") Some( diff --git a/metals/src/main/scala/scala/meta/internal/parsing/JavaTrees.scala b/metals/src/main/scala/scala/meta/internal/parsing/JavaTrees.scala index 3222769f0c7..fd2112c2cf6 100644 --- a/metals/src/main/scala/scala/meta/internal/parsing/JavaTrees.scala +++ b/metals/src/main/scala/scala/meta/internal/parsing/JavaTrees.scala @@ -17,12 +17,14 @@ import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals._ import scala.meta.io.AbsolutePath +import com.sun.source.tree.AnnotationTree import com.sun.source.tree.ClassTree import com.sun.source.tree.CompilationUnitTree import com.sun.source.tree.IdentifierTree import com.sun.source.tree.LineMap import com.sun.source.tree.MethodTree import com.sun.source.tree.Tree +import com.sun.source.tree.TypeCastTree import com.sun.source.tree.VariableTree import com.sun.source.util.TreePathScanner import com.sun.tools.javac.file.JavacFileManager @@ -90,20 +92,116 @@ class JavaTrees(buffers: Buffers) { } } yield result + /** + * Finds the variable declaration enclosing `pos`. + * + * @param onNameOnly when true, `pos` must be on the variable's name + * identifier; when false, anywhere within the declaration counts. + */ def findEnclosingJavaVariable( source: AbsolutePath, pos: l.Position, + onNameOnly: Boolean = true, ): Option[JavaVariable] = for { text <- text(source) tree <- get(source) result <- { - val visitor = new EnclosingVariableFinder(tree, text, pos) + val visitor = new EnclosingVariableFinder(tree, text, pos, onNameOnly) visitor.scan(tree, ()) visitor.result } } yield result + def findTypeCast( + source: AbsolutePath, + pos: l.Position, + ): Option[JavaTypeCast] = + for { + text <- text(source) + cu <- get(source) + castTree <- { + val visitor = new TypeCastFinder(cu, text, pos) + visitor.scan(cu, ()) + visitor.result + } + } yield { + val treePos = new TreePositions(cu) + val lineMap = + JavacPosition.makeLineMap(text.toCharArray(), text.length(), false) + val castStart = treePos.startPos(castTree) + val castEnd = treePos.endPos(castTree) + val typeEnd = treePos.endPos(castTree.getType()) + val exprStart = treePos.startPos(castTree.getExpression()) + val exprEnd = treePos.endPos(castTree.getExpression()) + var closeParenPos = typeEnd + while (closeParenPos < exprStart && text.charAt(closeParenPos) != ')') + closeParenPos += 1 + val typeRangeEnd = closeParenPos + 1 + JavaTypeCast( + range = JavaRange( + Positions.toLspRange(lineMap, castStart, castEnd, text), + castStart, + castEnd, + ), + typeRange = JavaRange( + Positions.toLspRange(lineMap, castStart, typeRangeEnd, text), + castStart, + typeRangeEnd, + ), + exprRange = JavaRange( + Positions.toLspRange(lineMap, exprStart, exprEnd, text), + exprStart, + exprEnd, + ), + ) + } + + def memberAnnotations( + source: AbsolutePath, + member: JavaMember, + ): List[JavaAnnotation] = + (for { + text <- text(source) + cu <- get(source) + } yield { + val treePos = new TreePositions(cu) + val lineMap = + JavacPosition.makeLineMap(text.toCharArray(), text.length(), false) + val rawAnnotations: Iterable[AnnotationTree] = member.tree match { + case m: MethodTree => m.getModifiers().getAnnotations().asScala + case v: VariableTree => v.getModifiers().getAnnotations().asScala + case c: ClassTree => c.getModifiers().getAnnotations().asScala + case _ => Iterable.empty + } + rawAnnotations.flatMap { ann => + val start = treePos.startPos(ann) + val end = treePos.endPos(ann) + if (start < 0 || end < 0) None + else { + val range = JavaRange( + Positions.toLspRange(lineMap, start, end, text), + startOffset = start, + endOffset = end, + ) + val nameStr = ann.getAnnotationType().toString() + val argsRange = + if (ann.getArguments().isEmpty()) None + else { + var openParen = start + 1 + while (openParen < end && text.charAt(openParen) != '(') + openParen += 1 + var closeParen = end - 1 + while (closeParen > openParen && text.charAt(closeParen) != ')') + closeParen -= 1 + if (openParen < closeParen) Some((openParen, closeParen)) + else None + } + Some(JavaAnnotation(nameStr, range, argsRange)) + } + }.toList + }).getOrElse(Nil) + private def text(source: AbsolutePath): Option[String] = buffers.get(source).orElse(source.readTextOpt) @@ -240,7 +338,10 @@ class JavaTrees(buffers: Buffers) { } .toList - protected def javaVariable(node: VariableTree): Option[JavaVariable] = { + protected def javaVariable( + node: VariableTree, + ownerKind: Tree.Kind, + ): Option[JavaVariable] = { val variableName = node.getName().toString() treeRange(node).map { range => JavaVariable( @@ -262,6 +363,7 @@ class JavaTrees(buffers: Buffers) { "var" }, modifiers = node.getModifiers().getFlags().asScala.toSet, + ownerKind = ownerKind, ) } } @@ -327,6 +429,7 @@ class JavaTrees(buffers: Buffers) { cu: CompilationUnitTree, text: String, targetPos: l.Position, + onNameOnly: Boolean, ) extends EnclosingFinder[JavaVariable](cu, text, targetPos) { override def visitVariable( @@ -342,8 +445,15 @@ class JavaTrees(buffers: Buffers) { .findNameOffset(text, nodeStart, nodeEnd, name) .getOrElse(nodeStart) val actualNodeEnd = actualNodeStart + name.length() - if (positionContains(targetOffset, actualNodeStart, actualNodeEnd)) { - _result = javaVariable(node) + if ( + !onNameOnly || + positionContains(targetOffset, actualNodeStart, actualNodeEnd) + ) { + val parentPath = getCurrentPath().getParentPath() + val ownerKind = + if (parentPath == null) Tree.Kind.COMPILATION_UNIT + else parentPath.getLeaf().getKind() + _result = javaVariable(node, ownerKind) } } super.visitVariable(node, p) @@ -434,7 +544,7 @@ class JavaTrees(buffers: Buffers) { ) } case field: VariableTree => - javaVariable(field) + javaVariable(field, Tree.Kind.CLASS) } .flatten .toList @@ -462,6 +572,20 @@ class JavaTrees(buffers: Buffers) { } } + private class TypeCastFinder( + cu: CompilationUnitTree, + text: String, + targetPos: l.Position, + ) extends EnclosingFinder[TypeCastTree](cu, text, targetPos) { + override def visitTypeCast(node: TypeCastTree, p: Unit): Unit = { + val nodeStart = pos.startPos(node) + val nodeEnd = pos.endPos(node) + if (positionContains(targetOffset, nodeStart, nodeEnd)) + _result = Some(node) + super.visitTypeCast(node, p) + } + } + private class TreePositions(cu: CompilationUnitTree) { private val endPosTable = cu.asInstanceOf[JavacJCTree.JCCompilationUnit].endPositions @@ -748,7 +872,22 @@ case class JavaVariable( nameRange: JavaRange, typ: String, modifiers: Set[Modifier], + ownerKind: Tree.Kind, ) extends JavaMember with HasModifiers { def hasInitializer: Boolean = tree.getInitializer() != null + def isStandaloneDeclaration: Boolean = + ownerKind != Tree.Kind.METHOD && ownerKind != Tree.Kind.TRY } + +case class JavaTypeCast( + range: JavaRange, + typeRange: JavaRange, + exprRange: JavaRange, +) + +case class JavaAnnotation( + name: String, + range: JavaRange, + argsRange: Option[(Int, Int)], +) diff --git a/mtags-java/src/main/scala/scala/meta/internal/jpc/JavaDiagnostics.scala b/mtags-java/src/main/scala/scala/meta/internal/jpc/JavaDiagnostics.scala index b8f12fb97c9..538121590d3 100644 --- a/mtags-java/src/main/scala/scala/meta/internal/jpc/JavaDiagnostics.scala +++ b/mtags-java/src/main/scala/scala/meta/internal/jpc/JavaDiagnostics.scala @@ -9,6 +9,8 @@ import javax.tools.Diagnostic.Kind.WARNING import javax.tools.JavaFileObject import com.sun.source.tree.LineMap +import com.sun.tools.javac.api.ClientCodeWrapper +import com.sun.tools.javac.util.JCDiagnostic import org.eclipse.{lsp4j => l} object JavaDiagnostics { @@ -28,7 +30,7 @@ object JavaDiagnostics { d.getEndPosition(), text ) - new l.Diagnostic( + val diagnostic = new l.Diagnostic( range, d.getMessage(null), d.getKind() match { @@ -45,5 +47,14 @@ object JavaDiagnostics { "javac", d.getCode() ) + val javacDiagnostic = d match { + case value: JCDiagnostic => Some(value) + case value: ClientCodeWrapper#DiagnosticSourceUnwrapper => Some(value.d) + case _ => None + } + javacDiagnostic + .filter(_.hasLintCategory()) + .foreach(value => diagnostic.setData(value.getLintCategory().option)) + diagnostic } } diff --git a/tests/unit/src/main/scala/tests/MbtJsonBuilder.scala b/tests/unit/src/main/scala/tests/MbtJsonBuilder.scala index 37659450e4e..1cbf2c332f2 100644 --- a/tests/unit/src/main/scala/tests/MbtJsonBuilder.scala +++ b/tests/unit/src/main/scala/tests/MbtJsonBuilder.scala @@ -80,12 +80,13 @@ case class MbtJsonBuilder( sources: List[String], dependsOn: List[String] = Nil, customScalaVersion: Option[String] = None, + javacOptions: List[String] = Nil, ): MbtJsonBuilder = { val distinctDeps = dependencyModules.distinctBy(_.id).reverse val namespace = MbtNamespace( sources = sources.asJava, scalacOptions = null, - javacOptions = null, + javacOptions = if (javacOptions.isEmpty) null else javacOptions.asJava, dependencyModules = distinctDeps.map(_.id).asJava, scalaVersion = customScalaVersion.getOrElse(scalaVersion), javaHome = null, diff --git a/tests/unit/src/main/scala/tests/codeactions/BaseCodeActionLspSuite.scala b/tests/unit/src/main/scala/tests/codeactions/BaseCodeActionLspSuite.scala index 5e43427abda..7230007306a 100644 --- a/tests/unit/src/main/scala/tests/codeactions/BaseCodeActionLspSuite.scala +++ b/tests/unit/src/main/scala/tests/codeactions/BaseCodeActionLspSuite.scala @@ -22,6 +22,7 @@ abstract class BaseCodeActionLspSuite( ) extends BaseLspSuite(suiteName, initializer) { protected val scalaVersion: String = V.scala213 + protected def javacOptions: List[String] = Nil /** * When set, `check` waits for a matching diagnostics publication before @@ -121,7 +122,11 @@ abstract class BaseCodeActionLspSuite( val layout = overrideLayout.getOrElse { if (useMbtLayout) { val mbtJson = new MbtJsonBuilder(scalaVersion) - .addNamespace("a", List("a/src/main/java/**", "a/src/main/scala/**")) + .addNamespace( + "a", + List("a/src/main/java/**", "a/src/main/scala/**"), + javacOptions = javacOptions, + ) .build() s"""/.metals/mbt.json |$mbtJson diff --git a/tests/unit/src/test/scala/tests/codeactions/RemoveRedundantCastLspSuite.scala b/tests/unit/src/test/scala/tests/codeactions/RemoveRedundantCastLspSuite.scala new file mode 100644 index 00000000000..6aad64435af --- /dev/null +++ b/tests/unit/src/test/scala/tests/codeactions/RemoveRedundantCastLspSuite.scala @@ -0,0 +1,368 @@ +package tests.codeactions + +import scala.meta.internal.metals.UserConfiguration +import scala.meta.internal.metals.codeactions.RemoveRedundantCast + +import org.eclipse.{lsp4j => l} +import tests.MbtTestInitializer + +class RemoveRedundantCastLspSuite + extends BaseCodeActionLspSuite( + "remove-redundant-cast", + MbtTestInitializer, + useMbtLayout = true, + ) { + + override def userConfig: UserConfiguration = + super.userConfig.copy( + presentationCompilerDiagnostics = true + ) + + override protected def javacOptions: List[String] = List("-Xlint:cast") + + override protected def toPath( + fileName: String, + isSource: Boolean = true, + ): String = + if (isSource) s"a/src/main/java/a/$fileName" + else s"a/$fileName" + + private val onlyRemoveRedundantCast: l.CodeAction => Boolean = + _.getTitle() == RemoveRedundantCast.title + + check( + "basic", + """|package a; + | + |public class Example { + | public String run() { + | return <<(String) "value">>; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public String run() { + | return "value"; + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "without-space", + """|package a; + | + |public class Example { + | public String run(String value) { + | return <<(String)value>>; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public String run(String value) { + | return value; + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "nested-parentheses", + """|package a; + | + |public class Example { + | public String run(String value) { + | return (<<(String) value>>); + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public String run(String value) { + | return (value); + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "generic-type", + """|package a; + | + |import java.util.List; + | + |public class Example { + | public String run(List list) { + | return (<<(List) list>>).get(0); + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |import java.util.List; + | + |public class Example { + | public String run(List list) { + | return (list).get(0); + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "array-type", + """|package a; + | + |public class Example { + | public String run(String[] array) { + | return (<<(String[]) array>>)[0]; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public String run(String[] array) { + | return (array)[0]; + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "primitive-type", + """|package a; + | + |public class Example { + | public int run() { + | return <<(int) 5>>; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public int run() { + | return 5; + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "method-invocation", + """|package a; + | + |public class Example { + | public String run(Object obj) { + | return <<(String) obj.toString()>>; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public String run(Object obj) { + | return obj.toString(); + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "extra-spaces", + """|package a; + | + |public class Example { + | public String run(String value) { + | return <<( String ) value>>; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public String run(String value) { + | return value; + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "newline-after-cast", + """|package a; + | + |public class Example { + | public String run(String value) { + | return <<(String) + | value>>; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public String run(String value) { + | return + | value; + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "variable-declaration", + """|package a; + | + |public class Example { + | public String run() { + | String s = <<(String) "value">>; + | return s; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public String run() { + | String s = "value"; + | return s; + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "this-cast", + """|package a; + | + |public class Example { + | public Example run() { + | return <<(Example) this>>; + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |public class Example { + | public Example run() { + | return this; + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "lambda-cast", + """|package a; + | + |import java.util.List; + |import java.util.stream.Collectors; + | + |public class Example { + | public List run(List list) { + | return list.stream().map(x -> <<(String) x>>).collect(Collectors.toList()); + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |import java.util.List; + |import java.util.stream.Collectors; + | + |public class Example { + | public List run(List list) { + | return list.stream().map(x -> x).collect(Collectors.toList()); + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) + + check( + "lambda-return-cast", + """|package a; + | + |import java.util.List; + |import java.util.stream.Collectors; + | + |public class Example { + | public List run(List list) { + | return list.stream().map(x -> { + | return <<(String) x>>; + | }).collect(Collectors.toList()); + | } + |} + |""".stripMargin, + s"""|${RemoveRedundantCast.title} + |""".stripMargin, + """|package a; + | + |import java.util.List; + |import java.util.stream.Collectors; + | + |public class Example { + | public List run(List list) { + | return list.stream().map(x -> { + | return x; + | }).collect(Collectors.toList()); + | } + |} + |""".stripMargin, + fileName = "Example.java", + filterAction = onlyRemoveRedundantCast, + ) +} diff --git a/tests/unit/src/test/scala/tests/codeactions/SuppressWarningsLspSuite.scala b/tests/unit/src/test/scala/tests/codeactions/SuppressWarningsLspSuite.scala new file mode 100644 index 00000000000..bae955b7ae1 --- /dev/null +++ b/tests/unit/src/test/scala/tests/codeactions/SuppressWarningsLspSuite.scala @@ -0,0 +1,896 @@ +package tests.codeactions + +import scala.meta.internal.metals.UserConfiguration +import scala.meta.internal.metals.codeactions.SuppressWarnings + +import munit.Location +import tests.MbtTestInitializer + +class SuppressWarningsLspSuite + extends BaseCodeActionLspSuite( + "suppress-warnings", + MbtTestInitializer, + useMbtLayout = true, + ) { + + override def userConfig: UserConfiguration = + super.userConfig.copy( + presentationCompilerDiagnostics = true + ) + + override protected def toPath( + fileName: String, + isSource: Boolean = true, + ): String = + if (isSource) s"a/src/main/java/a/$fileName" + else s"a/$fileName" + + private val deprecatedApiLayout = + """|/a/src/main/java/a/DeprecatedApi.java + |package a; + | + |class DeprecatedApi { + | @Deprecated + | static void old() {} + |} + |""".stripMargin + + checkSuppressWarnings( + "rawtypes-method", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + |<< public List names() { + | return new ArrayList(); + | }>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings("rawtypes") + | public List names() { + | return new ArrayList(); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "deprecation-method", + """|package a; + | + |public class Example { + |<< public void run() { + | DeprecatedApi.old(); + | }>> + |} + |""".stripMargin, + "compiler.warn.has.been.deprecated", + "deprecation", + """|package a; + | + |public class Example { + | @SuppressWarnings("deprecation") + | public void run() { + | DeprecatedApi.old(); + | } + |} + |""".stripMargin, + extraLayout = deprecatedApiLayout, + ) + + checkSuppressWarnings( + "serial-class", + """|package a; + | + |import java.io.Serializable; + | + |public class <> implements Serializable { + |} + |""".stripMargin, + "compiler.warn.missing.SVUID", + "serial", + """|package a; + | + |import java.io.Serializable; + | + |@SuppressWarnings("serial") + |public class Example implements Serializable { + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "append-existing", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings("unchecked") + |<< public List names() { + | return new ArrayList(); + | }>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings({"unchecked", "rawtypes"}) + | public List names() { + | return new ArrayList(); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "local-variable", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | public void run() { + |<< List list = new ArrayList<>();>> + | } + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | public void run() { + | @SuppressWarnings("rawtypes") + | List list = new ArrayList<>(); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "class-field", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + |<< private List names = new ArrayList<>();>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings("rawtypes") + | private List names = new ArrayList<>(); + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "constructor", + """|package a; + | + |public class Example { + |<< public Example() { + | DeprecatedApi.old(); + | }>> + |} + |""".stripMargin, + "compiler.warn.has.been.deprecated", + "deprecation", + """|package a; + | + |public class Example { + | @SuppressWarnings("deprecation") + | public Example() { + | DeprecatedApi.old(); + | } + |} + |""".stripMargin, + extraLayout = deprecatedApiLayout, + ) + + checkSuppressWarnings( + "append-existing-array", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings({"unchecked", "serial"}) + |<< public List names() { + | return new ArrayList(); + | }>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings({"unchecked", "serial", "rawtypes"}) + | public List names() { + | return new ArrayList(); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "append-existing-array-trailing-comma", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings({"unchecked",}) + |<< public List names() { + | return new ArrayList(); + | }>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings({"unchecked", "rawtypes"}) + | public List names() { + | return new ArrayList(); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "same-warning-multiple-declarations", + """|package a; + | + |import java.util.List; + | + |public class Example { + |<< public List first() { + | return List.of(); + | } + | + | public List second() { + | return List.of(); + | }>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.List; + | + |public class Example { + | public List first() { + | return List.of(); + | } + | + | @SuppressWarnings("rawtypes") + | public List second() { + | return List.of(); + | } + |} + |""".stripMargin, + expectedActionCount = 2, + selectedActionIndex = 1, + ) + + checkSuppressWarnings( + "append-existing-empty-array", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings({}) + |<< public List names() { + | return new ArrayList(); + | }>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings({"rawtypes"}) + | public List names() { + | return new ArrayList(); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "append-existing-named-scalar", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings(value = "unchecked") + |<< public List names() { + | return new ArrayList(); + | }>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings(value = {"unchecked", "rawtypes"}) + | public List names() { + | return new ArrayList(); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "append-existing-named-array", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings(value = {"unchecked", "serial"}) + |<< public List names() { + | return new ArrayList(); + | }>> + |} + |""".stripMargin, + "compiler.warn.raw.class.use", + "rawtypes", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings(value = {"unchecked", "serial", "rawtypes"}) + | public List names() { + | return new ArrayList(); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "multiple-annotations", + """|package a; + | + |public class Example implements Runnable { + | @Override + |<< public void run() { + | DeprecatedApi.old(); + | }>> + |} + |""".stripMargin, + "compiler.warn.has.been.deprecated", + "deprecation", + """|package a; + | + |public class Example implements Runnable { + | @Override + | @SuppressWarnings("deprecation") + | public void run() { + | DeprecatedApi.old(); + | } + |} + |""".stripMargin, + extraLayout = deprecatedApiLayout, + ) + + checkSuppressWarnings( + "cast-method", + """|package a; + | + |public class Example { + |<< public void run() { + | String value = (String) "value"; + | }>> + |} + |""".stripMargin, + "compiler.warn.redundant.cast", + "cast", + """|package a; + | + |public class Example { + | @SuppressWarnings("cast") + | public void run() { + | String value = (String) "value"; + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "dep-ann-class", + """|package a; + | + |/** + | * @deprecated use something else + | */ + |<> { + |} + |""".stripMargin, + "compiler.warn.missing.deprecated.annotation", + "dep-ann", + """|package a; + | + |/** + | * @deprecated use something else + | */ + |@SuppressWarnings("dep-ann") + |public class Example { + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "divzero-method", + """|package a; + | + |public class Example { + |<< public int run() { + | return 1 / 0; + | }>> + |} + |""".stripMargin, + "compiler.warn.div.zero", + "divzero", + """|package a; + | + |public class Example { + | @SuppressWarnings("divzero") + | public int run() { + | return 1 / 0; + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "empty-method", + """|package a; + | + |public class Example { + |<< public void run(boolean ok) { + | if (ok); + | }>> + |} + |""".stripMargin, + "compiler.warn.empty.if", + "empty", + """|package a; + | + |public class Example { + | @SuppressWarnings("empty") + | public void run(boolean ok) { + | if (ok); + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "fallthrough-method", + """|package a; + | + |public class Example { + |<< public void run(int value) { + | switch (value) { + | case 0: + | value++; + | case 1: + | value++; + | default: + | value++; + | } + | }>> + |} + |""".stripMargin, + "compiler.warn.possible.fall-through.into.case", + "fallthrough", + """|package a; + | + |public class Example { + | @SuppressWarnings("fallthrough") + | public void run(int value) { + | switch (value) { + | case 0: + | value++; + | case 1: + | value++; + | default: + | value++; + | } + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "finally-method", + """|package a; + | + |public class Example { + |<< public int run() { + | try { + | return 1; + | } finally { + | return 2; + | } + | }>> + |} + |""".stripMargin, + "compiler.warn.finally.cannot.complete", + "finally", + """|package a; + | + |public class Example { + | @SuppressWarnings("finally") + | public int run() { + | try { + | return 1; + | } finally { + | return 2; + | } + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "overrides-class", + """|package a; + | + |<> { + | @Override + | public boolean equals(Object other) { + | return other instanceof Example; + | } + |} + |""".stripMargin, + "compiler.warn.override.equals.but.not.hashcode", + "overrides", + """|package a; + | + |@SuppressWarnings("overrides") + |public class Example { + | @Override + | public boolean equals(Object other) { + | return other instanceof Example; + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "removal-method", + """|package a; + | + |public class Example { + |<< public void run() { + | RemovedApi.old(); + | }>> + |} + |""".stripMargin, + "compiler.warn.has.been.deprecated.for.removal", + "removal", + """|package a; + | + |public class Example { + | @SuppressWarnings("removal") + | public void run() { + | RemovedApi.old(); + | } + |} + |""".stripMargin, + extraLayout = """|/a/src/main/java/a/RemovedApi.java + |package a; + | + |class RemovedApi { + | @Deprecated(forRemoval = true) + | static void old() {} + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "unchecked-method", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + |<< public void run() { + | List raw = new ArrayList(); + | List strings = raw; + | }>> + |} + |""".stripMargin, + "compiler.warn.prob.found.req", + "unchecked", + """|package a; + | + |import java.util.ArrayList; + |import java.util.List; + | + |public class Example { + | @SuppressWarnings("unchecked") + | public void run() { + | List raw = new ArrayList(); + | List strings = raw; + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "static-method", + """|package a; + | + |public class Example { + | static int count; + | + |<< public void run(Example other) { + | other.count++; + | }>> + |} + |""".stripMargin, + "compiler.warn.static.not.qualified.by.type", + "static", + """|package a; + | + |public class Example { + | static int count; + | + | @SuppressWarnings("static") + | public void run(Example other) { + | other.count++; + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "strictfp-method", + """|package a; + | + |public class Example { + |<< public strictfp void run() { + | }>> + |} + |""".stripMargin, + "compiler.warn.strictfp", + "strictfp", + """|package a; + | + |public class Example { + | @SuppressWarnings("strictfp") + | public strictfp void run() { + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "synchronization-method", + """|package a; + | + |public class Example { + |<< public void run() { + | synchronized (Integer.valueOf(1)) { + | } + | }>> + |} + |""".stripMargin, + "compiler.warn.attempt.to.synchronize.on.instance.of.value.based.class", + "synchronization", + """|package a; + | + |public class Example { + | @SuppressWarnings("synchronization") + | public void run() { + | synchronized (Integer.valueOf(1)) { + | } + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "text-blocks-method", + s"""|package a; + | + |public class Example { + |<< public void run() { + | String text = ""\" + | trailing${" "} + | ""\"; + | }>> + |} + |""".stripMargin, + "compiler.warn.trailing.white.space.will.be.removed", + "text-blocks", + s"""|package a; + | + |public class Example { + | @SuppressWarnings("text-blocks") + | public void run() { + | String text = ""\" + | trailing${" "} + | ""\"; + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "try-method", + """|package a; + | + |import java.io.Closeable; + |import java.io.IOException; + | + |public class Example { + |<< public void run() throws IOException { + | try (Closeable closeable = null) { + | } + | }>> + |} + |""".stripMargin, + "compiler.warn.try.resource.not.referenced", + "try", + """|package a; + | + |import java.io.Closeable; + |import java.io.IOException; + | + |public class Example { + | @SuppressWarnings("try") + | public void run() throws IOException { + | try (Closeable closeable = null) { + | } + | } + |} + |""".stripMargin, + ) + + checkSuppressWarnings( + "varargs-method", + """|package a; + | + |import java.util.List; + | + |public class Example { + |<< public void run(List... lists) { + | }>> + |} + |""".stripMargin, + "compiler.warn.unchecked.varargs.non.reifiable.type", + "unchecked", + """|package a; + | + |import java.util.List; + | + |public class Example { + | @SuppressWarnings("unchecked") + | public void run(List... lists) { + | } + |} + |""".stripMargin, + ) + + private def checkSuppressWarnings( + name: String, + original: String, + diagnosticCode: String, + warningName: String, + expected: String, + extraLayout: String = "", + expectedActionCount: Int = 1, + selectedActionIndex: Int = 0, + )(implicit loc: Location): Unit = + test(name) { + val fileName = "Example.java" + val path = toPath(fileName) + val code = original.replace("<<", "").replace(">>", "") + + cleanWorkspace() + for { + _ <- initialize( + s"""|/.metals/mbt.json + |{ + | "namespaces": { + | "a": { + | "sources": ["a/src/main/java/**", "a/src/main/scala/**"], + | "javacOptions": ["-Xlint:all"] + | } + | } + |} + |/$path + |$code + |$extraLayout""".stripMargin + ) + _ <- server.didOpen(path) + diagnosticsPublished = + server.awaitNextDiagnostics( + path, + _.exists(diagnostic => + Option(diagnostic.getCode()).exists(code => + code.isLeft() && code.getLeft() == diagnosticCode + ) + ), + ) + _ <- server.didFocus(path) + _ <- diagnosticsPublished + codeActions <- server.assertCodeAction( + path, + original, + List + .fill(expectedActionCount)(SuppressWarnings.title(warningName)) + .mkString("\n"), + kind = Nil, + filterAction = _.getTitle() == SuppressWarnings.title(warningName), + ) + _ <- client.applyCodeAction(selectedActionIndex, codeActions, server) + _ <- server.didChange(path) { _ => + server.bufferContents(path) + } + _ <- server.didSave(path) + _ = assertNoDiff(server.bufferContents(path), expected) + } yield () + } +}