/**
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package com.python.pydev.refactoring.tdd;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.python.pydev.core.FullRepIterable;
import org.python.pydev.core.ICompletionCache;
import org.python.pydev.core.IDefinition;
import org.python.pydev.core.IPythonNature;
import org.python.pydev.core.IToken;
import org.python.pydev.core.MisconfigurationException;
import org.python.pydev.core.bundle.ImageCache;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.core.docutils.PySelection.LineStartingScope;
import org.python.pydev.core.docutils.PySelection.TddPossibleMatches;
import org.python.pydev.core.docutils.StringUtils;
import org.python.pydev.core.log.Log;
import org.python.pydev.core.structure.CompletionRecursionException;
import org.python.pydev.editor.PyEdit;
import org.python.pydev.editor.codecompletion.IPyCompletionProposal;
import org.python.pydev.editor.codecompletion.revisited.CompletionCache;
import org.python.pydev.editor.codecompletion.revisited.CompletionState;
import org.python.pydev.editor.codecompletion.revisited.CompletionStateFactory;
import org.python.pydev.editor.codecompletion.revisited.visitors.AssignDefinition;
import org.python.pydev.editor.codecompletion.revisited.visitors.Definition;
import org.python.pydev.editor.model.ItemPointer;
import org.python.pydev.editor.refactoring.AbstractPyRefactoring;
import org.python.pydev.editor.refactoring.IPyRefactoring;
import org.python.pydev.editor.refactoring.RefactoringRequest;
import org.python.pydev.parser.jython.ast.ClassDef;
import org.python.pydev.parser.jython.ast.FunctionDef;
import org.python.pydev.parser.jython.ast.Return;
import org.python.pydev.parser.visitors.NodeUtils;
import org.python.pydev.parser.visitors.scope.ASTEntry;
import org.python.pydev.parser.visitors.scope.EasyASTIteratorVisitor;
import org.python.pydev.parser.visitors.scope.ReturnVisitor;
import com.aptana.shared_core.callbacks.ICallback;
import com.python.pydev.analysis.ctrl_1.AbstractAnalysisMarkersParticipants;
import com.python.pydev.refactoring.refactorer.AstEntryRefactorerRequestConstants;
public class TddCodeGenerationQuickFixParticipant extends AbstractAnalysisMarkersParticipants {
private TddQuickFixParticipant tddQuickFixParticipant;
protected void fillParticipants() {
tddQuickFixParticipant = new TddQuickFixParticipant();
participants.add(tddQuickFixParticipant);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public List<ICompletionProposal> getProps(PySelection ps, ImageCache imageCache, File f, IPythonNature nature,
PyEdit edit, int offset) throws BadLocationException {
List<ICompletionProposal> ret = super.getProps(ps, imageCache, f, nature, edit, offset);
this.getTddProps(ps, imageCache, f, nature, edit, offset, ret);
return ret;
}
public List<ICompletionProposal> getTddProps(PySelection ps, ImageCache imageCache, File f, IPythonNature nature,
PyEdit edit, int offset, List<ICompletionProposal> ret) {
if (ret == null) {
ret = new ArrayList<ICompletionProposal>();
}
//Additional option: Generate markers for 'self.' accesses
int lineOfOffset = ps.getLineOfOffset(offset);
String lineContents = ps.getLine(lineOfOffset);
//Additional option: Generate methods for function calls
List<TddPossibleMatches> callsAtLine = ps.getTddPossibleMatchesAtLine();
if (callsAtLine.size() > 0) {
//Make sure we don't check the same thing twice.
Map<String, TddPossibleMatches> callsToCheck = new HashMap();
for (TddPossibleMatches call : callsAtLine) {
String callString = call.initialPart + call.secondPart;
callsToCheck.put(callString, call);
}
CONTINUE_FOR: for (Map.Entry<String, TddPossibleMatches> entry : callsToCheck.entrySet()) {
//we have at least something as SomeClass(a=2,c=3) or self.bar or self.foo.bar() or just foo.bar, etc.
IPyRefactoring pyRefactoring = AbstractPyRefactoring.getPyRefactoring();
try {
TddPossibleMatches possibleMatch = entry.getValue();
String callWithoutParens = entry.getKey();
ItemPointer[] pointers = null;
PySelection callPs = null;
TddPossibleMatches lastPossibleMatchNotFound = possibleMatch;
String lastCallWithoutParensNotFound = callWithoutParens;
for (int i = 0; i < 10; i++) { //more than 10 attribute accesses in a line? No way!
lastPossibleMatchNotFound = possibleMatch;
lastCallWithoutParensNotFound = callWithoutParens;
if (i > 0) {
//We have to take 1 level out of the match... i.e.: if it was self.foo.get(), search now for self.foo.
String line = FullRepIterable.getWithoutLastPart(possibleMatch.full);
List<TddPossibleMatches> tddPossibleMatchesAtLine = ps.getTddPossibleMatchesAtLine(line);
if (tddPossibleMatchesAtLine.size() > 0) {
possibleMatch = tddPossibleMatchesAtLine.get(0);
callWithoutParens = possibleMatch.initialPart + possibleMatch.secondPart;
} else {
continue CONTINUE_FOR;
}
}
String full = possibleMatch.full;
int indexOf = lineContents.indexOf(full);
if (indexOf < 0) {
Log.log("Did not expect index < 0.");
continue CONTINUE_FOR;
}
callPs = new PySelection(ps.getDoc(), ps.getLineOffset() + indexOf + callWithoutParens.length());
RefactoringRequest request = new RefactoringRequest(f, callPs, null, nature, edit);
//Don't look in additional info.
request.setAdditionalInfo(
AstEntryRefactorerRequestConstants.FIND_DEFINITION_IN_ADDITIONAL_INFO, false);
pointers = pyRefactoring.findDefinition(request);
if (((pointers != null && pointers.length > 0) || StringUtils.count(possibleMatch.full, '.') <= 1)) {
break;
}
}
if (pointers == null || callPs == null) {
continue CONTINUE_FOR;
}
if (lastPossibleMatchNotFound != null && lastPossibleMatchNotFound != possibleMatch
&& pointers.length >= 1) {
//Ok, as we were analyzing a string as self.bar.foo, we didn't find something in a pass
//i.e.: self.bar.foo, but we found it in a second pass
//as self.bar, so, this means we have to open the chance to create the 'foo' in self.bar.
String methodToCreate = FullRepIterable.getLastPart(lastPossibleMatchNotFound.secondPart);
int absoluteCursorOffset = callPs.getAbsoluteCursorOffset();
absoluteCursorOffset = absoluteCursorOffset - (1 + methodToCreate.length()); //+1 for the dot removed too.
PySelection newSelection = new PySelection(callPs.getDoc(), absoluteCursorOffset);
checkCreationBasedOnFoundPointers(edit, callPs, ret, possibleMatch, pointers, methodToCreate,
newSelection, nature);
continue CONTINUE_FOR;
}
if (pointers.length >= 1) {
//Ok, we found whatever was there, so, we don't need to create anything (except maybe do
//the __init__ or something at the class level).
if (!checkInitCreation(edit, callPs, pointers, ret)) {
//This was called only when isCall == false
//Ok, if it's not a call and we found a field, it's still possible that we may want to create
//a field if it wasn't found in the __init__
boolean foundInInit = false;
for (ItemPointer p : pointers) {
Definition definition = p.definition;
try {
Object peek = definition.scope.getScopeStack().peek();
if (peek instanceof FunctionDef) {
FunctionDef functionDef = (FunctionDef) peek;
String rep = NodeUtils.getRepresentationString(functionDef);
if (rep != null && rep.equals("__init__")) {
foundInInit = true;
break;
}
}
} catch (Exception e) {
}
}
if (!foundInInit) {
checkMethodCreationAtClass(edit, pyRefactoring, callWithoutParens, callPs, ret,
lineContents, possibleMatch, f, nature);
}
}
} else if (pointers.length == 0) {
checkMethodCreationAtClass(edit, pyRefactoring, callWithoutParens, callPs, ret, lineContents,
possibleMatch, f, nature);
}
} catch (Exception e) {
if (onGetTddPropsError != null) {
onGetTddPropsError.call(e);
}
Log.log(e);
}
}
}
return ret;
}
public static ICallback<Boolean, Exception> onGetTddPropsError;
private boolean checkMethodCreationAtClass(PyEdit edit, IPyRefactoring pyRefactoring, String callWithoutParens,
PySelection callPs, List<ICompletionProposal> ret, String lineContents, TddPossibleMatches possibleMatch,
File f, IPythonNature nature) throws MisconfigurationException, Exception {
RefactoringRequest request;
ItemPointer[] pointers;
//Ok, no definition found for the full string, so, check if we have a dot there and check
//if it could be a method in a local variable.
String[] headAndTail = FullRepIterable.headAndTail(callWithoutParens);
if (headAndTail[0].length() > 0) {
String methodToCreate = headAndTail[1];
if (headAndTail[0].equals("self")) {
//creating something in the current class -- note that if it was self.bar here, we'd treat it as regular
//(i.e.: no special support for self), so, we wouldn't enter here!
int firstCharPosition = PySelection.getFirstCharPosition(lineContents);
LineStartingScope scopeStart = callPs.getPreviousLineThatStartsScope(PySelection.CLASS_TOKEN, false,
firstCharPosition);
String classNameInLine = null;
if (scopeStart != null) {
for (Boolean isCall : new Boolean[] { true, false }) {
PyCreateMethodOrField pyCreateMethod = new PyCreateMethodOrField();
List<String> parametersAfterCall = null;
parametersAfterCall = configCreateAsAndReturnParametersAfterCall(callPs, isCall,
pyCreateMethod, parametersAfterCall, methodToCreate);
String startingScopeLineContents = callPs.getLine(scopeStart.iLineStartingScope);
classNameInLine = PySelection.getClassNameInLine(startingScopeLineContents);
if (classNameInLine != null && classNameInLine.length() > 0) {
pyCreateMethod.setCreateInClass(classNameInLine);
addCreateMethodOption(callPs, edit, ret, methodToCreate, parametersAfterCall,
pyCreateMethod, classNameInLine);
}
}
}
return true;
}
int absoluteCursorOffset = callPs.getAbsoluteCursorOffset();
absoluteCursorOffset = absoluteCursorOffset - (1 + methodToCreate.length()); //+1 for the dot removed too.
PySelection newSelection = new PySelection(callPs.getDoc(), absoluteCursorOffset);
request = new RefactoringRequest(f, newSelection, null, nature, edit);
//Don't look in additional info.
request.setAdditionalInfo(AstEntryRefactorerRequestConstants.FIND_DEFINITION_IN_ADDITIONAL_INFO, false);
pointers = pyRefactoring.findDefinition(request);
if (pointers.length == 1) {
if (checkCreationBasedOnFoundPointers(edit, callPs, ret, possibleMatch, pointers, methodToCreate,
newSelection, nature)) {
return true;
}
}
}
return false;
}
public Definition rebaseAssignDefinition(AssignDefinition assignDef, IPythonNature nature,
ICompletionCache completionCache) throws Exception {
//if the value is currently None, it will be set later on
if (assignDef.value.equals("None")) {
return assignDef; // keep the old one
}
//ok, go to the definition of whatever is set
IDefinition[] definitions2 = assignDef.module.findDefinition(
CompletionStateFactory.getEmptyCompletionState(assignDef.value, nature, completionCache),
assignDef.line, assignDef.col, nature);
if (definitions2.length > 0) {
return (Definition) definitions2[0];
}
return assignDef;
}
public Definition rebaseFunctionDef(Definition definition, IPythonNature nature, ICompletionCache completionCache)
throws Exception {
List<Return> returns = ReturnVisitor.findReturns((FunctionDef) definition.ast);
for (Return returnFound : returns) {
String act = NodeUtils.getFullRepresentationString(returnFound.value);
if (act == null) {
continue;
}
//ok, go to the definition of whatever is set
IDefinition[] definitions2 = definition.module.findDefinition(
CompletionStateFactory.getEmptyCompletionState(act, nature, completionCache), definition.line,
definition.col, nature);
if (definitions2.length == 1) {
return (Definition) definitions2[0];
}
}
return definition;
}
private Definition rebaseToClassDefDefinition(IPythonNature nature, CompletionCache completionCache,
Definition definition, CompletionState completionState) throws CompletionRecursionException, Exception {
if (completionState == null) {
completionState = new CompletionState();
}
if (definition.ast instanceof ClassDef) {
return definition;
}
if (definition instanceof AssignDefinition || definition.ast instanceof FunctionDef) {
//Avoid recursions.
completionState.checkDefinitionMemory(definition.module, definition);
if (definition instanceof AssignDefinition) {
definition = rebaseAssignDefinition((AssignDefinition) definition, nature, completionCache);
} else { // definition.ast MUST BE FunctionDef
definition = rebaseFunctionDef(definition, nature, completionCache);
}
return rebaseToClassDefDefinition(nature, completionCache, definition, completionState);
}
return definition;
}
public boolean checkCreationBasedOnFoundPointers(PyEdit edit, PySelection callPs, List<ICompletionProposal> ret,
TddPossibleMatches possibleMatch, ItemPointer[] pointers, String methodToCreate, PySelection newSelection,
IPythonNature nature) throws MisconfigurationException, Exception {
CompletionCache completionCache = new CompletionCache();
for (ItemPointer pointer : pointers) {
Definition definition = pointer.definition;
try {
definition = rebaseToClassDefDefinition(nature, completionCache, definition, null);
} catch (CompletionRecursionException e) {
Log.log(e); //Just keep going.
}
if (definition.ast instanceof ClassDef) {
ClassDef d = (ClassDef) definition.ast;
String fullName = NodeUtils.getRepresentationString(d) + "." + methodToCreate;
IToken repInModule = nature.getAstManager().getRepInModule(definition.module, fullName, nature);
if (repInModule != null) {
//System.out.println("Skipping creation of: " + fullName); //We found it, so, don't suggest it.
continue;
}
for (Boolean isCall : new Boolean[] { true, false }) {
//Give the user a chance to create the method we didn't find.
PyCreateMethodOrField pyCreateMethod = new PyCreateMethodOrField();
List<String> parametersAfterCall = null;
parametersAfterCall = configCreateAsAndReturnParametersAfterCall(callPs, isCall, pyCreateMethod,
parametersAfterCall, methodToCreate);
String className = NodeUtils.getRepresentationString(d);
pyCreateMethod.setCreateInClass(className);
String displayString = com.aptana.shared_core.string.StringUtils.format("Create %s %s at %s (%s)", methodToCreate,
pyCreateMethod.getCreationStr(), className, definition.module.getName());
TddRefactorCompletionInModule completion = new TddRefactorCompletionInModule(methodToCreate,
tddQuickFixParticipant != null ? tddQuickFixParticipant.imageMethod : null, displayString,
null, displayString, IPyCompletionProposal.PRIORITY_CREATE, edit,
definition.module.getFile(), parametersAfterCall, pyCreateMethod, newSelection);
completion.locationStrategy = AbstractPyCreateAction.LOCATION_STRATEGY_END;
ret.add(completion);
}
return true;
}
}
return false;
}
private List<String> configCreateAsAndReturnParametersAfterCall(PySelection callPs, boolean isCall,
PyCreateMethodOrField pyCreateMethod, List<String> parametersAfterCall, String methodToCreate) {
if (isCall) {
pyCreateMethod.setCreateAs(PyCreateMethodOrField.BOUND_METHOD);
parametersAfterCall = callPs.getParametersAfterCall(callPs.getAbsoluteCursorOffset());
} else {
if (StringUtils.isAllUpper(methodToCreate)) {
pyCreateMethod.setCreateAs(PyCreateMethodOrField.CONSTANT);
} else {
pyCreateMethod.setCreateAs(PyCreateMethodOrField.FIELD);
}
}
return parametersAfterCall;
}
private void addCreateMethodOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props,
String markerContents, List<String> parametersAfterCall, PyCreateMethodOrField pyCreateMethod,
String classNameInLine) {
String displayString = com.aptana.shared_core.string.StringUtils.format("Create %s %s at %s", markerContents,
pyCreateMethod.getCreationStr(), classNameInLine);
TddRefactorCompletion tddRefactorCompletion = new TddRefactorCompletion(markerContents,
tddQuickFixParticipant.imageMethod, displayString, null, null, IPyCompletionProposal.PRIORITY_CREATE,
edit, PyCreateClass.LOCATION_STRATEGY_BEFORE_CURRENT, parametersAfterCall, pyCreateMethod, ps);
props.add(tddRefactorCompletion);
}
private boolean checkInitCreation(PyEdit edit, PySelection callPs, ItemPointer[] pointers,
List<ICompletionProposal> ret) {
for (ItemPointer pointer : pointers) {
Definition definition = pointer.definition;
if (definition != null && definition.ast instanceof ClassDef) {
ClassDef d = (ClassDef) definition.ast;
ASTEntry initEntry = findInitInClass(d);
if (initEntry == null) {
//Give the user a chance to create the __init__.
PyCreateMethodOrField pyCreateMethod = new PyCreateMethodOrField();
pyCreateMethod.setCreateAs(PyCreateMethodOrField.BOUND_METHOD);
String className = NodeUtils.getRepresentationString(d);
pyCreateMethod.setCreateInClass(className);
List<String> parametersAfterCall = callPs.getParametersAfterCall(callPs.getAbsoluteCursorOffset());
String displayString = com.aptana.shared_core.string.StringUtils.format("Create %s __init__ (%s)", className,
definition.module.getName());
TddRefactorCompletionInModule completion = new TddRefactorCompletionInModule("__init__",
tddQuickFixParticipant.imageMethod, displayString, null, displayString,
IPyCompletionProposal.PRIORITY_CREATE, edit, definition.module.getFile(),
parametersAfterCall, pyCreateMethod, callPs);
completion.locationStrategy = AbstractPyCreateAction.LOCATION_STRATEGY_FIRST_METHOD;
ret.add(completion);
return true;
}
}
}
return false;
}
public static ASTEntry findInitInClass(ClassDef d) {
EasyASTIteratorVisitor visitor = EasyASTIteratorVisitor.create(d);
for (Iterator<ASTEntry> it = visitor.getMethodsIterator(); it.hasNext();) {
ASTEntry next = it.next();
if (next.node != null) {
String rep = NodeUtils.getRepresentationString(next.node);
if ("__init__".equals(rep)) {
return next;
}
}
}
return null;
}
}