/** * Copyright (c) 2010, 2011 Darmstadt University of Technology. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Marcel Bruch - initial API and implementation. */ package org.eclipse.recommenders.internal.chain.rcp; import static org.eclipse.recommenders.internal.chain.rcp.TypeBindingAnalyzer.*; import static org.eclipse.recommenders.internal.chain.rcp.l10n.LogMessages.WARNING_CANNOT_HANDLE_FOR_FINDING_ENTRY_POINTS; import static org.eclipse.recommenders.utils.Logs.log; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.internal.codeassist.InternalCompletionContext; import org.eclipse.jdt.internal.codeassist.complete.CompletionOnMemberAccess; import org.eclipse.jdt.internal.codeassist.complete.CompletionOnMessageSend; import org.eclipse.jdt.internal.codeassist.complete.CompletionOnQualifiedAllocationExpression; import org.eclipse.jdt.internal.codeassist.complete.CompletionOnQualifiedNameReference; import org.eclipse.jdt.internal.codeassist.complete.CompletionOnSingleNameReference; import org.eclipse.jdt.internal.compiler.ast.ASTNode; import org.eclipse.jdt.internal.compiler.lookup.Binding; import org.eclipse.jdt.internal.compiler.lookup.FieldBinding; import org.eclipse.jdt.internal.compiler.lookup.InvocationSite; import org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding; import org.eclipse.jdt.internal.compiler.lookup.Scope; import org.eclipse.jdt.internal.compiler.lookup.TypeBinding; import org.eclipse.jdt.internal.compiler.lookup.VariableBinding; import org.eclipse.jdt.internal.compiler.util.ObjectVector; import org.eclipse.jdt.internal.ui.text.template.contentassist.TemplateProposal; import org.eclipse.jdt.ui.PreferenceConstants; import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext; import org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer; import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.recommenders.completion.rcp.CompletionContextKey; import org.eclipse.recommenders.completion.rcp.DisableContentAssistCategoryJob; import org.eclipse.recommenders.completion.rcp.IRecommendersCompletionContext; import org.eclipse.recommenders.completion.rcp.RecommendersCompletionContext; import org.eclipse.recommenders.internal.chain.rcp.ChainRcpModule.ChainCompletion; import org.eclipse.recommenders.rcp.IAstProvider; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.util.concurrent.SimpleTimeLimiter; @SuppressWarnings("restriction") public class ChainCompletionProposalComputer implements IJavaCompletionProposalComputer { public static final String CATEGORY_ID = "org.eclipse.recommenders.chain.rcp.proposalCategory.chain"; //$NON-NLS-1$ private IRecommendersCompletionContext ctx; private List<ChainElement> entrypoints; private String error; private final IPreferenceStore prefStore; private Scope scope; private InvocationSite invocationSite; private IAstProvider astProvider; @Inject public ChainCompletionProposalComputer(IAstProvider astProvider, @ChainCompletion final IPreferenceStore preferenceStore) { this.astProvider = astProvider; prefStore = preferenceStore; } @Override public List<ICompletionProposal> computeCompletionProposals(final ContentAssistInvocationContext context, final IProgressMonitor monitor) { if (!shouldMakeProposals()) { return Collections.emptyList(); } if (!initializeRequiredContext(context)) { return Collections.emptyList(); } if (!shouldPerformCompletionOnExpectedType()) { return Collections.emptyList(); } if (!findEntrypoints()) { return Collections.emptyList(); } return executeCallChainSearch(); } @VisibleForTesting /** * Ensures that we only make recommendations if we are not on the default tab. Disables this engine if the user has * activated chain completion on default content assist list */ protected boolean shouldMakeProposals() { final Set<String> excluded = Sets.newHashSet(PreferenceConstants.getExcludedCompletionProposalCategories()); if (excluded.contains(CATEGORY_ID)) { // we are excluded on default tab? Then we are not on default tab NOW. We are on a subsequent tab and should // make completions: return true; } // disable and stop computing. new DisableContentAssistCategoryJob(CATEGORY_ID).schedule(300); return false; } private boolean initializeRequiredContext(final ContentAssistInvocationContext context) { if (!(context instanceof JavaContentAssistInvocationContext)) { return false; } JavaContentAssistInvocationContext jdtCtx = (JavaContentAssistInvocationContext) context; ctx = new RecommendersCompletionContext(jdtCtx, astProvider); final Optional<Scope> optionalScope = ScopeAccessWorkaround.resolveScope(ctx); if (!optionalScope.isPresent()) { return false; } scope = optionalScope.get(); return true; } private boolean shouldPerformCompletionOnExpectedType() { final Optional<IType> expected = ctx.getExpectedType(); return expected.isPresent() || !ctx.getExpectedTypeNames().isEmpty(); } private boolean findEntrypoints() { entrypoints = new LinkedList<ChainElement>(); final ASTNode node = ctx.getCompletionNode().orNull(); if (node instanceof CompletionOnQualifiedNameReference) { invocationSite = (CompletionOnQualifiedNameReference) node; findEntrypointsForCompletionOnQualifiedName((CompletionOnQualifiedNameReference) node); } else if (node instanceof CompletionOnMemberAccess) { invocationSite = (CompletionOnMemberAccess) node; findEntrypointsForCompletionOnMemberAccess((CompletionOnMemberAccess) node); } else if (node instanceof CompletionOnSingleNameReference || node instanceof CompletionOnQualifiedAllocationExpression || node instanceof CompletionOnMessageSend) { invocationSite = (InvocationSite) node; findEntrypointsForCompletionOnSingleName(); } return !entrypoints.isEmpty(); } private void findEntrypointsForCompletionOnQualifiedName(final CompletionOnQualifiedNameReference node) { final Binding b = node.binding; if (b == null) { return; } switch (b.kind()) { case Binding.TYPE: addPublicStaticMembersToEntrypoints((TypeBinding) b); break; case Binding.FIELD: addPublicInstanceMembersToEntrypoints(((FieldBinding) b).type); break; case Binding.LOCAL: addPublicInstanceMembersToEntrypoints(((VariableBinding) b).type); break; default: log(WARNING_CANNOT_HANDLE_FOR_FINDING_ENTRY_POINTS, b); } } private void addPublicStaticMembersToEntrypoints(final TypeBinding type) { for (final Binding m : findAllPublicStaticFieldsAndNonVoidNonPrimitiveStaticMethods(type, invocationSite, scope)) { if (matchesExpectedPrefix(m)) { entrypoints.add(new ChainElement(m, false)); } } } private void addPublicInstanceMembersToEntrypoints(final TypeBinding type) { for (final Binding m : findVisibleInstanceFieldsAndRelevantInstanceMethods(type, invocationSite, scope)) { if (matchesExpectedPrefix(m)) { entrypoints.add(new ChainElement(m, false)); } } } private boolean matchesExpectedPrefix(final Binding binding) { return String.valueOf(binding.readableName()).startsWith(ctx.getPrefix()); } private void findEntrypointsForCompletionOnMemberAccess(final CompletionOnMemberAccess node) { final TypeBinding b = node.actualReceiverType; if (b == null) { return; } addPublicInstanceMembersToEntrypoints(b); } private void findEntrypointsForCompletionOnSingleName() { InternalCompletionContext context = ctx.get(CompletionContextKey.INTERNAL_COMPLETIONCONTEXT, null); ObjectVector visibleLocalVariables = context.getVisibleLocalVariables(); Set<String> localVariableNames = getLocalVariableNames(visibleLocalVariables); resolveEntrypoints(visibleLocalVariables, localVariableNames); resolveEntrypoints(context.getVisibleFields(), localVariableNames); resolveEntrypoints(context.getVisibleMethods(), localVariableNames); } private static Set<String> getLocalVariableNames(final ObjectVector visibleLocalVariables) { final Set<String> names = Sets.newHashSet(); for (int i = visibleLocalVariables.size(); i-- > 0;) { final LocalVariableBinding decl = (LocalVariableBinding) visibleLocalVariables.elementAt(i); names.add(Arrays.toString(decl.name)); } return names; } private void resolveEntrypoints(final ObjectVector elements, final Set<String> localVariableNames) { for (int i = elements.size(); i-- > 0;) { final Binding decl = (Binding) elements.elementAt(i); if (!matchesPrefixToken(decl)) { continue; } final String key = String.valueOf(decl.computeUniqueKey()); if (key.startsWith("Ljava/lang/Object;")) { //$NON-NLS-1$ continue; } boolean requiresThis = false; if (decl instanceof FieldBinding) { requiresThis = localVariableNames.contains(Arrays.toString(((FieldBinding) decl).name)); } final ChainElement e = new ChainElement(decl, requiresThis); if (e.getReturnType() != null) { entrypoints.add(e); } } } private boolean matchesPrefixToken(final Binding decl) { return String.valueOf(decl.readableName()).startsWith(ctx.getPrefix()); } private List<ICompletionProposal> executeCallChainSearch() { final int maxChains = prefStore.getInt(ChainsPreferencePage.PREF_MAX_CHAINS); final int minDepth = prefStore.getInt(ChainsPreferencePage.PREF_MIN_CHAIN_LENGTH); final int maxDepth = prefStore.getInt(ChainsPreferencePage.PREF_MAX_CHAIN_LENGTH); final String[] excludedTypes = prefStore.getString(ChainsPreferencePage.PREF_IGNORED_TYPES).split("\\|"); //$NON-NLS-1$ for (int i = 0; i < excludedTypes.length; ++i) { excludedTypes[i] = "L" + excludedTypes[i].replace('.', '/'); //$NON-NLS-1$ } final List<Optional<TypeBinding>> expectedTypes = TypeBindingAnalyzer.resolveBindingsForExpectedTypes(ctx, scope); final ChainFinder finder = new ChainFinder(expectedTypes, Sets.newHashSet(excludedTypes), invocationSite, scope); try { new SimpleTimeLimiter().callWithTimeout(new Callable<Void>() { @Override public Void call() { finder.startChainSearch(entrypoints, maxChains, minDepth, maxDepth); return null; } }, prefStore.getInt(ChainsPreferencePage.PREF_TIMEOUT), TimeUnit.SECONDS, true); } catch (final Exception e) { setError("Timeout during call chain computation."); //$NON-NLS-1$ } return buildCompletionProposals(finder.getChains()); } private List<ICompletionProposal> buildCompletionProposals(final List<Chain> chains) { final List<ICompletionProposal> proposals = Lists.newLinkedList(); for (final Chain chain : chains) { final TemplateProposal proposal = CompletionTemplateBuilder.create(chain, ctx.getJavaContext()); final ChainCompletionProposal completionProposal = new ChainCompletionProposal(proposal, chain); proposals.add(completionProposal); } return proposals; } private void setError(final String errorMessage) { error = errorMessage; } @Override public List<IContextInformation> computeContextInformation(final ContentAssistInvocationContext context, final IProgressMonitor monitor) { return Collections.emptyList(); } @Override public void sessionStarted() { setError(null); } @Override public String getErrorMessage() { return error; } @Override public void sessionEnded() { } }