/* * Copyright 2009-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.codehaus.groovy.eclipse.quickfix.proposals; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.codehaus.groovy.eclipse.codeassist.relevance.RelevanceRules; import org.codehaus.groovy.eclipse.quickfix.GroovyQuickFixPlugin; import org.codehaus.groovy.eclipse.refactoring.actions.TypeSearch; import org.codehaus.groovy.eclipse.refactoring.actions.TypeSearch.UnresolvedTypeData; import org.codehaus.jdt.groovy.model.GroovyCompilationUnit; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.SourceRange; import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; import org.eclipse.jdt.core.search.TypeNameMatch; import org.eclipse.jdt.internal.ui.JavaPluginImages; import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.TextEdit; /** * Generates quick fix proposals for a unresolved type in a Groovy class. Each * proposal is associated with a Java IType that can be imported to resolve the * unresolved type. Therefore, if the resolver finds n types that can possibly * be imported, the resolver will generate 5 different proposals, one for each * suggested type. Each proposal has its own display string indicating which * type will be imported. * * @author Nieraj Singh */ public class AddMissingGroovyImportsResolver extends AbstractQuickFixResolver { public AddMissingGroovyImportsResolver(QuickFixProblemContext problem) { super(problem); } public static class AddMissingImportProposal extends AbstractGroovyQuickFixProposal { private IType resolvedSuggestedType; private GroovyCompilationUnit unit; public AddMissingImportProposal(IType resolvedSuggestedType, GroovyCompilationUnit unit, QuickFixProblemContext problem, int relevance) { super(problem, relevance); this.resolvedSuggestedType = resolvedSuggestedType; this.unit = unit; } public IType getSuggestedJavaType() { return resolvedSuggestedType; } protected String getImageBundleLocation() { return JavaPluginImages.IMG_OBJS_IMPDECL; } protected ImportRewrite getImportRewrite() { ImportRewrite rewriter = null; try { rewriter = ImportRewrite.create(unit, true); } catch (Exception e) { GroovyQuickFixPlugin.log(e); } return rewriter; } public void apply(IDocument document) { ImportRewrite rewrite = getImportRewrite(); if (rewrite != null) { rewrite.addImport(getSuggestedJavaType().getFullyQualifiedName('.')); try { TextEdit edit = rewrite.rewriteImports(null); if (edit != null) { unit.applyTextEdit(edit, null); } } catch (Exception e) { GroovyQuickFixPlugin.log(e); } } } public String getDisplayString() { IType declaringType = getSuggestedJavaType().getDeclaringType(); // For inner types, display the fully qualified top-level type as // the declaration for the suggested type String declaration = declaringType != null ? declaringType.getFullyQualifiedName().replace('$', '.') : getSuggestedJavaType().getPackageFragment().getElementName(); return "Import '" + getSuggestedJavaType().getElementName() + "' (" + declaration + ")"; } } protected ProblemType[] getTypes() { return new ProblemType[] {ProblemType.MISSING_IMPORTS_TYPE}; } /** * Returns the type suggestions that may resolve the unresolved type problem. * * @return list of type suggestions for the unresolved type, or null if nothing is found */ protected List<IType> getImportTypeSuggestions() { int offset = getQuickFixProblem().getOffset(); try { String simpleTypeName = getUnresolvedSimpleName(); if (simpleTypeName != null) { boolean isAnnotation = getQuickFixProblem().getProblemDescriptor().getMarkerMessages()[0].contains("@" + simpleTypeName); UnresolvedTypeData data = new UnresolvedTypeData(simpleTypeName, isAnnotation, new SourceRange(offset, simpleTypeName.length())); new TypeSearch().searchForTypes(getGroovyCompilationUnit(), Collections.singletonMap(simpleTypeName, data), null); List<TypeNameMatch> matches = data.getFoundInfos(); if (matches != null && !matches.isEmpty()) { List<IType> suggestions = new ArrayList<IType>(matches.size()); for (TypeNameMatch match : matches) { suggestions.add(match.getType()); } return suggestions; } } } catch (JavaModelException e) { GroovyQuickFixPlugin.log(e); } return null; } /** * Obtains the simple name of the unresolved type from the quick fix problem * * @return simple name of the unresolved type. */ protected String getUnresolvedSimpleName() { // NOTE: for now get the type from the message String. Not elegant, but // computationally and logically much simpler than checking the AST for // the correct declaration node that may contain unresolved type // information // especially since the location information for the problem does not // directly point to the actual unresolved type in source, but rather // surrounding identifiers like variable names, method names, and even // Java key words. String[] messages = getQuickFixProblem().getProblemDescriptor().getMarkerMessages(); if (messages == null || messages.length == 0) { return null; } for (String text : ProblemType.MISSING_IMPORTS_TYPE.groovyProblemSnippets) { int startIndex = messages[0].indexOf(text); if (startIndex >= 0) { Pattern pattern = Pattern.compile("\\b\\w+\\b"); Matcher matcher = pattern.matcher(messages[0].substring(startIndex + text.length())); if (matcher.find()) { return getTopLevelType(matcher.group()); } } } return null; } /** * If the simple name is an Inner Type, this will return the top level type. * If it already is a top level type, the name will be returned as is. For * inner types that are declared with the declaring (top level) type (e.g. * Map.Entry), only the top level type is needed to import the inner type. * * @param simpleName * whose top level type should be obtained * @return top level simple name of the given simple name. If it already is * top level, return the same simple name that was passed as a * argument. */ protected static String getTopLevelType(String simpleName) { int firstIndex = simpleName != null ? simpleName.indexOf('.') : -1; if (firstIndex >= 0) { simpleName = simpleName.substring(0, firstIndex); } return simpleName; } public List<IJavaCompletionProposal> getQuickFixProposals() { List<IType> suggestions = getImportTypeSuggestions(); if (suggestions != null && !suggestions.isEmpty()) { List<IJavaCompletionProposal> fixes = new ArrayList<IJavaCompletionProposal>(suggestions.size()); for (IType type : suggestions) { fixes.add(new AddMissingImportProposal(type, getGroovyCompilationUnit(), getQuickFixProblem(), getRelevance(type))); } return fixes; } return null; } /** * @return non-null Groovy compilation unit containing the problem that this * resolver should fix. */ protected GroovyCompilationUnit getGroovyCompilationUnit() { return (GroovyCompilationUnit) getQuickFixProblem().getCompilationUnit(); } protected int getRelevance(IType type) { if (type == null) return 0; return RelevanceRules.ALL_RULES.getRelevance(type, getContextTypes()); } }