From cb2e25c629c0da8d6d6784dd7289d168d08dfd3a Mon Sep 17 00:00:00 2001 From: Ivan Kuraj Date: Tue, 21 Aug 2012 16:57:55 +0200 Subject: [PATCH] implemented an extendible quick fix functionality for type mismatch errors --- .../quickfix/ScalaQuickFixProcessor.scala | 72 ++++++++--- .../TypeMismatchQuickFixProcessor.scala | 114 ++++++++++++++++++ 2 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 org.scala-ide.sdt.core/src/scala/tools/eclipse/quickfix/TypeMismatchQuickFixProcessor.scala diff --git a/org.scala-ide.sdt.core/src/scala/tools/eclipse/quickfix/ScalaQuickFixProcessor.scala b/org.scala-ide.sdt.core/src/scala/tools/eclipse/quickfix/ScalaQuickFixProcessor.scala index d388609d35..5e75e23b9f 100644 --- a/org.scala-ide.sdt.core/src/scala/tools/eclipse/quickfix/ScalaQuickFixProcessor.scala +++ b/org.scala-ide.sdt.core/src/scala/tools/eclipse/quickfix/ScalaQuickFixProcessor.scala @@ -1,10 +1,12 @@ package scala.tools.eclipse.quickfix +// Eclipse +import org.eclipse.core.runtime.CoreException +import org.eclipse.core.runtime.NullProgressMonitor import org.eclipse.ui.IEditorPart -import org.eclipse.jface.text.source.Annotation +import org.eclipse.ui.texteditor.ITextEditor import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitDocumentProvider.ProblemAnnotation import org.eclipse.jdt.ui.JavaUI -import org.eclipse.core.runtime.CoreException import org.eclipse.jdt.core.ICompilationUnit import org.eclipse.jdt.core.search.IJavaSearchConstants import org.eclipse.jdt.internal.codeassist.ISearchRequestor @@ -12,22 +14,32 @@ import org.eclipse.jdt.internal.compiler.env.{ AccessRestriction } import org.eclipse.jdt.internal.core.{ DefaultWorkingCopyOwner, JavaProject } import org.eclipse.jdt.internal.ui.text.correction.{ SimilarElement, SimilarElementsRequestor } import org.eclipse.jdt.ui.text.java._ -import org.eclipse.core.resources.IMarker +import org.eclipse.jdt.core.search.{ TypeNameMatch, SearchEngine } +import org.eclipse.jdt.core.IJavaElement +import org.eclipse.jdt.internal.corext.util.TypeNameMatchCollector +import org.eclipse.jface.text.source.Annotation +import org.eclipse.jface.text.Position +import org.eclipse.jface.text.IDocument + +// Scala IDE import scala.tools.eclipse.javaelements.ScalaSourceFile import scala.tools.eclipse.util.FileUtils +import scala.tools.eclipse.util.EditorUtils.getAnnotationsAtOffset +import scala.tools.eclipse.semantichighlighting.implicits.ImplicitHighlightingPresenter +import scala.tools.eclipse.logging.HasLogger + +// Scala import scala.util.matching.Regex -import org.eclipse.jdt.core.search.TypeNameMatch -import org.eclipse.jdt.core.search.SearchEngine -import org.eclipse.jdt.core.IJavaElement -import org.eclipse.jdt.internal.corext.util.TypeNameMatchCollector -import org.eclipse.core.runtime.NullProgressMonitor import collection.JavaConversions._ -class ScalaQuickFixProcessor extends IQuickFixProcessor { +class ScalaQuickFixProcessor extends IQuickFixProcessor with HasLogger { private val typeNotFoundError = new Regex("not found: type (.*)") private val valueNotFoundError = new Regex("not found: value (.*)") private val xxxxxNotFoundError = new Regex("not found: (.*)") + // regex for extracting expected and required type on type mismatch + private val typeMismatchError = new Regex("type mismatch;\\s*found\\s*: (\\S*)\\s*required: (.*)") + /** * Checks if the processor has any corrections. * @@ -36,7 +48,6 @@ class ScalaQuickFixProcessor extends IQuickFixProcessor { * slightly more responsive. */ def hasCorrections(unit : ICompilationUnit, problemId : Int) : Boolean = true - /** * Collects corrections or code manipulations for the given context. @@ -53,9 +64,15 @@ class ScalaQuickFixProcessor extends IQuickFixProcessor { val editor = JavaUI.openInEditor(context.getCompilationUnit) var corrections : List[IJavaCompletionProposal] = Nil for (location <- locations) - for (ann <- getAnnotationsAtOffset(editor, location.getOffset)) { - val fix = suggestFix(context.getCompilationUnit(), ann.getText) - corrections = corrections ++ fix + for ((ann, pos) <- getAnnotationsAtOffset(editor, location.getOffset)) { + val importFix = suggestImportFix(context.getCompilationUnit(), ann.getText) + + // compute all possible type mismatch quick fixes + val document = (editor.asInstanceOf[ITextEditor]).getDocumentProvider().getDocument(editor.getEditorInput()) + val typeMismatchFix = suggestTypeMismatchFix(document, ann.getText, pos) + + // concatenate lists of found quick fixes + corrections = corrections ++ importFix ++ typeMismatchFix } corrections match { case Nil => null @@ -65,7 +82,8 @@ class ScalaQuickFixProcessor extends IQuickFixProcessor { case _ => null } - private def getAnnotationsAtOffset(part: IEditorPart, offset: Int): List[Annotation] = { + // XXX is this code duplication? -- check scala.tools.eclipse.util.EditorUtils.getAnnotationsAtOffset + private def getAnnotationsAtOffsetXXX(part: IEditorPart, offset: Int): List[Annotation] = { import ScalaQuickFixProcessor._ var ret = List[Annotation]() @@ -81,7 +99,7 @@ class ScalaQuickFixProcessor extends IQuickFixProcessor { } private - def suggestFix(compilationUnit : ICompilationUnit, problemMessage : String) : List[IJavaCompletionProposal] = { + def suggestImportFix(compilationUnit : ICompilationUnit, problemMessage : String) : List[IJavaCompletionProposal] = { /** * Import a type could solve several error message : * @@ -106,6 +124,30 @@ class ScalaQuickFixProcessor extends IQuickFixProcessor { case _ => Nil } } + private + def suggestTypeMismatchFix(document : IDocument, problemMessage : String, location: Position) : List[IJavaCompletionProposal] = { + // get the annotation string + val annotationString = document.get(location.getOffset, location.getLength) + // match problem message + return problemMessage match { + // extract found and required type + case typeMismatchError(foundType, requiredType) => + // utilize type mismatch computer to find quick fixes + val replacementStringList = TypeMismatchQuickFixProcessor(foundType, requiredType, annotationString) + + // map replacements strings into expanding proposals + replacementStringList map { + replacementString => + // make markers message in form: "... =>replacement" + val markersMessage = annotationString + " " + ImplicitHighlightingPresenter.DisplayStringSeparator + replacementString + // construct a proposal with the appropriate location + new ExpandingProposalBase(markersMessage, "Transform your code expression: ", location) + } + // no match found for the problem message + case _ => Nil + } + } + } object ScalaQuickFixProcessor { diff --git a/org.scala-ide.sdt.core/src/scala/tools/eclipse/quickfix/TypeMismatchQuickFixProcessor.scala b/org.scala-ide.sdt.core/src/scala/tools/eclipse/quickfix/TypeMismatchQuickFixProcessor.scala new file mode 100644 index 0000000000..330c656d84 --- /dev/null +++ b/org.scala-ide.sdt.core/src/scala/tools/eclipse/quickfix/TypeMismatchQuickFixProcessor.scala @@ -0,0 +1,114 @@ +package scala.tools.eclipse.quickfix + +// Java imports +import java.util.regex.Pattern + +/** + * @author ivcha + * This object is used for applying code transformations based on the found and required type + * extract from the annotation message (such as quick fix message) and the expression in the source code. + * The object arguments are: found type string, required type string and annotation string, respective and + * the result is a list of strings which should replace the annotation string + */ +object TypeMismatchQuickFixProcessor extends + ((String, String, String) => List[String]) { + + /** list containing all type mismatch quick fix cases that this object should go through */ + val cases: List[TypeMismatchQuickFixCase] = + List( + // "type mismatch: List[T] but found List[List[T]] + FoundToRequiredTypeCase( + List("%s.flatten", "%s.head", "%s.last"), + Pattern.compile("List\\[List\\[(.*)\\]\\]"), Pattern.compile("List\\[(.*)\\]"), Pattern.compile("^(.*)$") + ), + // "type mismatch: Array[T] but found List[T] + FoundToRequiredTypeCase( + List("%s.toArray"), + Pattern.compile("List\\[(.*)\\]"), Pattern.compile("Array\\[(.*)\\]"), Pattern.compile("^(.*)$") + ), + // "type mismatch: found T; required Option[T]" -> suggest to wrap the result in Some() + FoundToRequiredTypeCase( + List("Some(%s)"), + Pattern.compile("(.*)"), Pattern.compile("Option\\[(.*)\\]"), Pattern.compile("^(.*)$") + ) + ) + + /** + * apply method for getting list of replacement strings + * @param foundType extracted found type string + * @param requiredType extracted required type string + * @param annotationString extracted expression string from the source code + * @return list of strings which should replace the annotation string + */ + def apply(foundType: String, requiredType: String, annotationString: String): List[String] = + // go through all cases and collect lists of replacement strings + (List[String]() /: cases) { + case (list, ftrtc:FoundToRequiredTypeCase) => { + list ++ ftrtc.apply(foundType, requiredType, annotationString) + } + } + +} + +/** trait marking all type mismatch quick fix cases */ +trait TypeMismatchQuickFixCase + +/** + * class which is to be inherited if quick fix simply injects a sequence of strings into a format strings of + * form "... %s... %s..." + */ +abstract class SimpleFormatQuickFixCase(formatStrings: List[String]) extends TypeMismatchQuickFixCase { + def apply(listOfInjectStrings: Seq[String]*) = + for ( + // iterate through all sequences of strings to inject + injectString <- listOfInjectStrings; + // iterate through all given format + formatString <- formatStrings + // yield a string when inject strings are applied to the format string + ) yield { formatString.format( injectString:_* ) } +} + +/** + * class which checks whether found type string and required type string match - it does by + * capturing all groups according to the found and required patterns and compares them for match - + * if all match, the replacement is proceeded by extracting inject strings from the annotation pattern + * and applying them to SimpleFormatQuickFixCase + * found and required patterns should extract the same number of groups + * annotation string should extract required number of groups to feed the given format string + */ +case class FoundToRequiredTypeCase(formatStrings: List[String], + found: Pattern, required: Pattern, annotationExtract: Pattern) extends SimpleFormatQuickFixCase(formatStrings) { + def apply(foundType: String, requiredType: String, annotationString: String): Seq[String] = { + // get matchers + val foundMatcher = found.matcher(foundType) + val requiredMatcher = required.matcher(requiredType) + + // if both matched + // NOTE (we expect only a single match) + if (foundMatcher.find && requiredMatcher.find) { + // check if all groups match + if ( + // fold all groups and compare them - capturing group count must be the same for both patterns + (true /: (1 to foundMatcher.groupCount()) ) { + case (false, _) => false + case (result, ind) => foundMatcher.group(ind) == requiredMatcher.group(ind) + } + ) { + // get annotation matcher + val annotationMatcher = annotationExtract.matcher(annotationString) + // check if find can pass (only single match is expected) + if (annotationMatcher.find) { + // get injection strings + val injectStrings = + for (ind <- 1 to annotationMatcher.groupCount()) yield { annotationMatcher.group(ind) } + // apply them to the format string + super.apply(injectStrings) + // in case annotation matcher cannot find + } else Nil + } + // in case groups don't match + else Nil + // in case matchers fail + } else Nil + } +} \ No newline at end of file