package pyUML.pythonTree; import java.util.Dictionary; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.uml2.uml.Class; import org.eclipse.uml2.uml.Classifier; import org.eclipse.uml2.uml.Comment; import org.eclipse.uml2.uml.Interface; import org.eclipse.uml2.uml.NamedElement; import org.eclipse.uml2.uml.Operation; import org.eclipse.uml2.uml.Parameter; import org.eclipse.uml2.uml.Property; import org.eclipse.uml2.uml.VisibilityKind; import org.python.pydev.parser.jython.ast.Expr; import org.python.pydev.parser.jython.ast.FunctionDef; import org.python.pydev.parser.jython.ast.Name; import org.python.pydev.parser.jython.ast.NameTok; import org.python.pydev.parser.jython.ast.Str; import org.python.pydev.parser.jython.ast.exprType; import org.python.pydev.parser.jython.ast.stmtType; import pyUML.backend.GlobalConstants; import pyUML.backend.ParseHelpers; import pyUML.exceptions.PyUMLParseException; import pyUML.exceptions.PyUMLSynchronizeCodeException; import pyUML.refactoring.BicycleRefactoring; import pyUML.refactoring.FileRefactoring; /** * This class represents a Method definition inside a Python Class */ public class PythonTreeMethod extends PythonTreeNode { private FunctionDef astNode; private boolean staticMethod; private List arguments; private String docString; private int docStringLine; private int docStringCol; private int lastLine; private String indent = " "; private Dictionary argumentDefaultDict; private Dictionary argumentLineDict; private Dictionary argumentColumnDict; public PythonTreeMethod(PythonTreeClass parent, FunctionDef astNode, int nextLine) throws PyUMLParseException{ super(parent); this.astNode = astNode; this.lastLine = nextLine - 1; // set last non-empty line as last line of this method while (this.lastLine>=0 && parent.inFile.getSplittedFileContent()[lastLine-1].matches("[\\s]*")) { this.lastLine--; } this.arguments = new Vector(); this.argumentDefaultDict = new Hashtable(); this.argumentLineDict = new Hashtable(); this.argumentColumnDict = new Hashtable(); this.initMethod(); } public void initMethod() throws PyUMLParseException{ this.name = ((NameTok) this.astNode.name).id; if (this.name.matches(".*__init__.*") && this.getParent().getName().matches(".*CMCEntryBrows.*")) { } exprType[] argList = this.astNode.args.args; exprType[] defaultList = this.astNode.args.defaults; // ### Get Arguments ### // // get offset of first argument with default value int argCount = argList.length; int defaultCount = defaultList.length; int defaultOffset = argCount - defaultCount; // get arguments with corresponding default values int argNo = 0; for (exprType arg : argList) { // fix indent, if needed String line = this.getParent().getInFile().getSplittedFileContent()[arg.beginLine ]; int indentLength=0; if (line.length() > 0) for (; line.charAt(indentLength) == ' ' && line.length() > indentLength +1 ; indentLength++) ; if (this.indent.length() != indentLength && ! line.matches("^[\\s]*\\\"\\\"\\\".*$")) { this.indent = ""; for (int i=0; i< indentLength; i++) this.indent += " "; } // argument name String argName = ((Name) arg).id; if (this.arguments.contains(argName)) { String message = "Argument inconsistency detected!\n"+ "The python method '"+this.name+ "' in the class '" + this.getParent().getName() + "'\nhas an argument inconsistency with the argument '" + argName + "'.\n\n" + "The argument exists mor than once!\n\n" + "Please correct this error!"; throw new PyUMLParseException(message); } this.arguments.add(argName); this.argumentLineDict.put(argName, arg.beginLine); this.argumentColumnDict.put(argName, arg.beginColumn); // argument default value if (defaultCount> 0 && argNo >= defaultOffset && (! argName.equals("self"))) { exprType defaultVal = defaultList[argNo - defaultOffset]; exprType defaultValBefore = null; // test if parameter without default follows parameter with default // -> this is not allowed but not caught by pyDev if (argNo - defaultOffset >= 1) defaultValBefore = defaultList[argNo - defaultOffset-1]; String defValString = ParseHelpers.getStringOfExpr(defaultVal, this.getParent().getInFile(), false, false, 0); if (defaultVal == null && defaultValBefore!= null) { String message = "Argument inconsistency detected!\n"+ "The python method '"+this.name+ "' in the class '" + this.getParent().getName() + "'\nhas an argument inconsistency with the argument '" + argName + "'.\n\n" + "There are arguments without default values" + "\nfollowing arguments with default values.\n\n" + "Please correct this error!"; throw new PyUMLParseException(message); } else if (defaultVal != null){ this.argumentDefaultDict.put(argName, defValString); } } argNo++; } // set static attribute */ if (argList.length == 0 || (! this.arguments.get(0).equals("self"))) this.staticMethod = true; stmtType[] methodBody = this.astNode.body; // extract DocString, if it exists if (methodBody.length > 0 && methodBody[0] instanceof Expr) { Expr exp = (Expr) methodBody[0]; if (exp.value instanceof Str) { Str stringExp = (Str) exp.value; if (stringExp.type == Str.TripleDouble || stringExp.type == Str.TripleSingle) { this.docString=stringExp.s; this.docStringLine = stringExp.beginLine; this.docStringCol = stringExp.beginColumn; } } } } public boolean synchronizeModel(NamedElement modelElement) { super.synchronizeModel(modelElement); Operation modelMethod = (Operation) modelElement; // set this method private in model, if its name starts with "__" // use protected, if class starts with "_" (this is only python convention) if (this.getName().startsWith("__")) { modelMethod.setVisibility(VisibilityKind.PRIVATE_LITERAL); } else if (this.getName().startsWith("_")) { modelMethod.setVisibility(VisibilityKind.PROTECTED_LITERAL); } // set/remove static flag if (this.staticMethod != modelMethod.isStatic()) modelMethod.setIsStatic(this.staticMethod); List parameterList = modelMethod.getOwnedParameters(); // for all parameters in code -> check if model parameters are the same; /// if not, add them for (int i=0; i< this.getArguments().size(); i++) { String pyArg = this.getArguments().get(i); if (parameterList.size() > i) { Parameter modelParam = parameterList.get(i); // check if parameter name and default value are the same String defautlValue = (this.getArgumentDefaultDict().get(pyArg)); if (defautlValue == null) defautlValue = ""; if ((! pyArg.equals(modelParam.getName())) || ( ! defautlValue.equals(modelParam.getDefault()))) { // this parameter is not the same! // remove all parameters from here and create following parameters! for (int j=i; j< parameterList.size(); j++) parameterList.remove(j); // restart loop at the current position (again) i--; } } else { // Parameter at this position does not exist // Append new parameter Parameter newParam = modelMethod.createOwnedParameter(pyArg, null); newParam.setDefault(this.getArgumentDefaultDict().get(pyArg)); } } // test, if there are additional model parameters -> remove them! if (parameterList.size() > this.getArguments().size()) { for (int i=this.getArguments().size(); i < parameterList.size(); i++) { parameterList.get(i).destroy(); } } return true; } public boolean synchronizeCode(Operation modelOp) throws PyUMLSynchronizeCodeException{ if (this.name.matches("search.*") && this.getParent().getName().startsWith("Group")) { } List modelParameters = new Vector(); int line = this.astNode.name.beginLine; boolean parametersNeedRewrite = false; boolean modelIsStatic = true; methodChildLoop: for (Parameter modelParameter : modelOp.getOwnedParameters()) { // get all relevant data String modelParName = modelParameter.getName(); modelParameters.add(modelParName); // stop if this is not a parameter (null) if (modelParName == null) continue methodChildLoop; if (modelParName.equals("self")) { modelIsStatic = false; } String modelParDefault = modelParameter.getDefault(); if (this.arguments.contains(modelParName)){ //found parameter with same name! String codeParDefault = this.getArgumentDefaultDict().get(modelParName); if (((modelParDefault == codeParDefault)&&(codeParDefault== null)) || (modelParDefault != null && codeParDefault != null && (codeParDefault.equals(modelParDefault) || (modelParDefault.equals(GlobalConstants.getUnknownExpression()))))) // parameter exists and has the same default value // everything is fine! continue methodChildLoop; // parameter exists, but with different default! // fix default value! // if default value does not exist in model if (modelParDefault == null) { parametersNeedRewrite = true; continue methodChildLoop; } else if (codeParDefault == null) { //if default value wasn't there //add a default value parametersNeedRewrite = true; continue methodChildLoop; } else { // default value changed // -> change default value parametersNeedRewrite = true; continue methodChildLoop; } } else { // parameter with this name not found! // look if parameter was renamed (look in old model) // this is done with bicycle repair man, so that // all references will be renamed, too String paramXMIId = this.getRoot().getModelXmiDict().get(modelParameter); if (this.getRoot().getXmiModelDictOld().keySet().contains(paramXMIId)) { // parameter found -> try to find old name in code Parameter oldModelParam = (Parameter) this.getRoot().getXmiModelDictOld().get(paramXMIId); String oldParamName = oldModelParam.getName(); if (this.arguments.contains(oldParamName)) { // parameter with old name found -> rename it! int col = this.argumentColumnDict.get(oldParamName); BicycleRefactoring.doRenameObject(this.getParent().inFile, modelParName, line, col, this.getRoot().getProject()); setChanged(line, modelParName); return true; } } } //parameter does and did not exist in code //create it! parametersNeedRewrite = true; continue methodChildLoop; } // check if parameters were deleted -> delete them in code! for (String parameter : this.arguments) { if (! modelParameters.contains(parameter)) { //parameter does not exist in model -> delete it parametersNeedRewrite = true; break; } } // if method changed its static property, // ensure the "staticmethod" call is added/removed if (this.fixStaticmethodCall(modelIsStatic)) return true; if (parametersNeedRewrite) { // write new parameters definition String parametersDef = ""; for (Parameter modelParameter: modelOp.getOwnedParameters()) { // get all relevant data String modelParName = modelParameter.getName(); // stop if this is not a parameter (null) if (modelParName == null) continue; String modelParDefault = modelParameter.getDefault(); if (parametersDef.length() > 0) parametersDef += ", "; parametersDef += modelParName; if (modelParDefault != null) parametersDef += "=" + modelParDefault; } // replace old definition with help of regex Pattern pattern = Pattern.compile("([\\s]*def[\\s]*[^\\(]+[\\s]*\\()[^\\)]*(\\)[\\s]*:.*)"); String defLine = FileRefactoring.getLine(this.getParent().inFile, line); Matcher matcher = pattern.matcher(defLine); if (! matcher.find()){ return false; } // write the new method definition line String newDefLine = matcher.group(1) + parametersDef + matcher.group(2); FileRefactoring.replaceLine(this.getParent().inFile, line, newDefLine, true); setChanged(line, newDefLine); return true; } return false; } /** * On synchronizing code: * Test the presence of all attributes * - add/rename them, if needed * - check initial value and fix it, if needed * * @param modelClass the class in the model where the attributes are defined * @return true, if something was changed; false otherwise */ public static boolean fixAttributeDefinitions( Classifier modelClass, String initIndent, Map presentAttrs, PythonTreeClass pyClass, int lastLine, boolean isStatic) throws PyUMLSynchronizeCodeException{ // if this is the __init__ method, ensure all non-static attributes are defined here! //Map presentAttrs = pyClass.getChildObjectAttrDict(); List neededAttrs = null; if (modelClass instanceof Class) neededAttrs = ((Class) modelClass).getOwnedAttributes(); else if (modelClass instanceof Interface) neededAttrs = ((Interface) modelClass).getOwnedAttributes(); for (Property neededAttr : neededAttrs) { if (isStatic != neededAttr.isStatic()) continue; String attrName = neededAttr.getName(); String initialValue = neededAttr.getDefault(); // if initial value was abbreviated before, try to get // full value out of model if (initialValue != null && initialValue.endsWith("(...)")) { for (Comment comment :neededAttr.getOwnedComments()) { if (comment.getBody().startsWith(GlobalConstants.getFullDefault())) { initialValue = comment.getBody().substring(GlobalConstants.getFullDefault().length()); break; } } } if (! presentAttrs.containsKey(attrName)) { // attribute is not present -> find out, if it was renamed in model String param_xmi = pyClass.getRoot().getModelXmiDict().get(neededAttr); if (pyClass.getRoot().getXmiModelDictOld().containsKey(param_xmi)) { Property oldParameter = (Property) pyClass.getRoot().getXmiModelDictOld().get(param_xmi); String oldParamName = oldParameter.getName(); if (presentAttrs.containsKey(oldParamName)) { // attribute was renamed -> rename it in code with Bicycle Repair Man int line = presentAttrs.get(oldParamName).getLine(); int col = presentAttrs.get(oldParamName).getCol(); boolean success = BicycleRefactoring.doRenameObject( pyClass.inFile, attrName, line, col, pyClass.getRoot().getProject()); return success; } } // attribute is and was not present -> create it! String attrDefLine = (isStatic ? pyClass.getIndent() : initIndent+"self.")+ attrName + " = "; if (initialValue != null && (! initialValue.endsWith("(...)"))) attrDefLine += initialValue; else attrDefLine += "None"; attrDefLine += " # created by PyUML"; if (lastLine <=0 ) FileRefactoring.appendToFile(pyClass.inFile, attrDefLine); else FileRefactoring.insertAtLine(pyClass.inFile, lastLine, attrDefLine + "\n"); pyClass.getRoot().setChangedFileLine(pyClass.inFile.getFilePath().toOSString()+":"+lastLine + attrDefLine + ":"); return true; } else { // attribute is defined, check initial value // if initial model value ends with (...), // this value was cut when creating the model // -> do not change it in code PythonTreeAttribute pyAttr = presentAttrs.get(attrName); if (pyAttr.getValue() != null && initialValue != null) { } if ((!pyAttr.getValue().equals(initialValue)) && (!(pyAttr.getValue().equals("None") && initialValue==null)) && (! initialValue.endsWith("(...)")) && (! initialValue.startsWith(pyAttr.getValue().replaceAll("\\(\\.\\.\\.\\)$", ""))) ){ // default value changed! int line = pyAttr.getLine(); Pattern pattern = Pattern.compile("([\\s]*"+(isStatic?"":"self.")+"[^=]*=[\\s]*)([^#]+)(.*)"); String defLine = FileRefactoring.getLine(pyClass.getInFile(), line); Matcher matcher = pattern.matcher(defLine); if (! matcher.find()){ return false; } String initString = initialValue; if (initString == null) initString = "None"; // write the new method definition line String commentGroup = matcher.group(3); if (commentGroup.startsWith("#")) commentGroup = " " + commentGroup; String newDefLine = matcher.group(1) + initString + commentGroup; // if this was a multi-line attribute, delete // all following lines and preserver comments for (int i = line ; i < pyAttr.getNextLine() - 1; i++) pyClass.getInFile().setFileContent(FileRefactoring.removeLine(pyClass.inFile, i, false, true)); // now, just replace the changed line FileRefactoring.replaceLine(pyClass.inFile, line, newDefLine, true); pyClass.getRoot().setChangedFileLine(pyClass.inFile.getFilePath().toOSString()+":"+line); return true; } } } return false; } /** * On synchronizing code: * Test staticmethod call of this method and * fix it, if needed * * (python static methods must have the call * = staticmethod() * after the method definition) * * @param modelIsStatic true, if this method should be static * in code and vice versa * @return true, if something was changed * false, otherwise */ public boolean fixStaticmethodCall(boolean modelIsStatic) throws PyUMLSynchronizeCodeException{ // ensure there is a staticmethod call after the method String fileContent[] = this.getParent().inFile.getFileContent().split("\n"); String lineContent = ""; int lineNo = this.lastLine; if (this.lastLine <= 0) lineNo= fileContent.length -1; // if next command is "staticmethod", it is meant to be // part of *this* method if (fileContent.length > lineNo + 1 && fileContent[lineNo+1].matches("^[\\s]*[^=\\(\\s]*[\\s]*=[\\s]*staticmethod[\\s]*\\([\\s]*[^\\)\\s]*[\\s]*\\)[\\s]*.*")) { // the next command is the line to change lineNo++; } // find the last non-empty non-comment line for (; lineNo>=0; lineNo--){ lineContent = fileContent[lineNo]; if (! (lineContent.matches("^[\\s]*$") || lineContent.matches("^[\\s]*#.*$")) ) break; } if (lineContent.matches("^[\\s]*[^=\\(\\s]*[\\s]*=[\\s]*staticmethod[\\s]*\\([\\s]*[^\\)\\s]*[\\s]*\\)[\\s]*.*")) { // there is already a staticmethod call if (!modelIsStatic) { // staticmethod call has to be removed FileRefactoring.removeLine(this.getParent().inFile, lineNo, true, false); return true; } // ensure staticmethod call is OK if (lineContent.matches("[\\s]*"+this.getName()+"[\\s]*=[\\s]*staticmethod[\\s]*\\([^\\)\\s]*\\)[\\s]*.*")){ // staticmethod call alright, nothing needs to be changed return false; } else { // fix staticmethod call (e.g. when method was renamed) String newContent = this.getParent().getIndent() + this.getName()+" = staticmethod("+this.getName()+")"; FileRefactoring.replaceLine(this.getParent().inFile, lineNo+1, newContent, true); setChanged(lineNo, newContent); return true; } } else { // there is no staticmethod call at all if (!modelIsStatic) { // all is OK return false; } else { //-> insert it after the end of the method String newContent = this.getParent().getIndent()+this.getName()+" = staticmethod("+this.getName()+")"; lineNo = this.lastLine; if (this.lastLine <= 0) lineNo = fileContent.length -1; // from the next method, find the first before line with whitespace for (; lineNo>=0; lineNo--){ lineContent = fileContent[lineNo]; if (! (lineContent.matches("^[\\s]*$"))) break; } FileRefactoring.insertAtLine(this.getParent().inFile, lineNo + 2, newContent + "\n"); setChanged(lineNo, newContent); return true; } } } /** * tells the position of current code change to PythonTreeRoot * so that it can decide if the same line is changed again * and again and thus detect an unfinished loop. * * @param line the changed line in the file */ private void setChanged(int line, String pos) throws PyUMLSynchronizeCodeException{ if (pos == null) pos = ""; else pos = ":position:"+pos; this.getRoot().setChangedFileLine(getParent().inFile.getFilePath().toOSString()+":"+line+pos); } public boolean isStatic() { return staticMethod; } public PythonTreeClass getParent() { return (PythonTreeClass) super.getParent(); } public String getDocString() { return docString; } public int getDocStringLine() { return docStringLine; } public int getDocStringCol() { return docStringCol; } public FunctionDef getAstNode() { return astNode; } public List getArguments() { return arguments; } public Dictionary getArgumentDefaultDict() { return argumentDefaultDict; } public int getLastLine() { return lastLine; } public String getIndent() { return indent; } }