FEATURE: Added import support for imports without "from" and with "as" statements

NOTE: I disabled import fixing, to avoid duplicate / wrong imports, should be reviewed and adapted to the new import detection capabilities
This commit is contained in:
jakob_oswald 2009-01-12 10:41:06 +00:00
parent bb149cb7f2
commit 19ec7a8c8d
1 changed files with 220 additions and 92 deletions

View File

@ -19,6 +19,7 @@ import org.eclipse.core.runtime.IPath;
import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gmf.runtime.common.core.util.StringUtil;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.uml2.uml.Association;
import org.eclipse.uml2.uml.Class;
@ -35,6 +36,7 @@ import org.eclipse.uml2.uml.Type;
import org.eclipse.uml2.uml.VisibilityKind;
import org.python.pydev.parser.jython.ast.Assign;
import org.python.pydev.parser.jython.ast.Attribute;
import org.python.pydev.parser.jython.ast.Break;
import org.python.pydev.parser.jython.ast.ClassDef;
import org.python.pydev.parser.jython.ast.Expr;
import org.python.pydev.parser.jython.ast.FunctionDef;
@ -61,7 +63,10 @@ import pyUML.refactoring.FileRefactoring;
* - can write XMI ID to classes docstring
*
*/
public class PythonTreeClass extends PythonTreeNode{
public class PythonTreeClass extends PythonTreeNode {
private static final Pattern FROM_IMPORT_PATTERN = Pattern.compile("[\\s]*from[\\s]*([^\\s]*)[\\s]*import[\\s]*([^\\s#]*)([\\s#]?.*)");
ClassDef astNode;
String docString;
PythonTreeFile inFile;
@ -74,7 +79,16 @@ public class PythonTreeClass extends PythonTreeNode{
Map<String, PythonTreeMethod> childMethodDict;
Map<String, PythonTreeAttribute> childStaticAttrDict;
Map<String, PythonTreeAttribute> childObjectAttrDict;
/**
* A map holding the full package name in the format mypackage/mysubpackage/MyClass
* indexed by the classname which might be the plain classname if it has been
* imported using a "from mypackage.mysubpackage import MyClass" statement
* or prefixed with the package / alias if imported with a simple
* "import mypackage.mysubpackage" or "import mypackage.mysubpackage as myalias".
*/
private Map<String, String> packageCache;
/**
* Constructor to create a PythonTreeClass of a python class AST
* @param parent the PythonTreePackage, which is parent of this class
@ -92,13 +106,14 @@ public class PythonTreeClass extends PythonTreeNode{
this.childMethodDict = new Hashtable<String, PythonTreeMethod>();
this.childStaticAttrDict = new Hashtable<String, PythonTreeAttribute>();
this.childObjectAttrDict = new Hashtable<String, PythonTreeAttribute>();
packageCache = new HashMap<String, String>();
this.initClassNode();
this.initSubStructures();
}
private void initClassNode() throws PyUMLParseException, PyUMLCancelledException{
this.name=((NameTok) this.astNode.name).id;
this.name = ((NameTok)this.astNode.name).id;
this.docStringLine = this.astNode.beginLine;
this.docStringCol = this.astNode.beginColumn;
@ -131,7 +146,7 @@ public class PythonTreeClass extends PythonTreeNode{
PythonTreeRoot root = this.getRoot();
// test if a class of this name already exists
String structuredName = this.getPackageStructure()+this.name;
String structuredName = this.getPackageStructure() + this.name;
this.getRoot().setSubTaskName(structuredName);
if (root.getClassDict().containsKey(structuredName) && root.isShowWarnings()) {
@ -155,15 +170,16 @@ public class PythonTreeClass extends PythonTreeNode{
root.getClassDict().put(structuredName, this);
root.getClassNameDict().put(structuredName, this.name);
for (int i=0; i < this.astNode.bases.length; i++) {
for (int i = 0; i < this.astNode.bases.length; i++) {
exprType superClass = this.astNode.bases[i];
// check whether it's a plain string, which means a classname
// or separated by dots
if (superClass instanceof Name) {
String superName = ((Name)superClass).id;
String superClassPackString = this.getPackFromImports(superName);
if (superClassPackString != null)
this.superClasses.add(superClassPackString);
}
else if (superClass instanceof Attribute) {
this.superClasses.add(superClassPackString);
} else if (superClass instanceof Attribute) {
// Just extract the content in the paranthesis without
// any further analysis
// This can be a subclass or a class under a package
@ -187,9 +203,13 @@ public class PythonTreeClass extends PythonTreeNode{
String superClassFullName = line.substring(0, line.indexOf(separator));
superClassFullName = superClassFullName.replaceAll("[\\s]", "");
this.superClasses.add(superClassFullName);
// currently we suppose that the class will be prefixed by
// either the fully qualified package name or an alias,
// nested classes are not supported yet!
String superClassPackString = this.getPackFromImports(superClassFullName);
if (superClassPackString != null) { // TODO: will a null value ever return?
this.superClasses.add(superClassPackString);
}
}
}
}
@ -990,75 +1010,105 @@ public class PythonTreeClass extends PythonTreeNode{
* @return true, if anything was changed, false otherwise
*/
public boolean fixImports(Classifier modelClass) throws PyUMLSynchronizeCodeException{
// iterate over all superclasses in project
Map<String, String> superClassPackages = new HashMap<String, String>();
for (PythonTreeClass superClass : this.getModelGeneralizationsInProject(modelClass)) {
// get package structure, e.g. mypackage.subpackage.MyClass
String packageDef = "";
PythonTreePackage superPackage = superClass.getParent();
while (superPackage != null && (! (superPackage instanceof PythonTreeRoot))) {
packageDef = superPackage.getName() + "." + packageDef;
superPackage = superPackage.getParent();
}
String superClassModuleName = superClass.inFile.getName().replace(".py", "");
packageDef += superClassModuleName;
superClassPackages.put(superClass.getName(), packageDef);
return false; // See issues in FIXME below
// // iterate over all superclasses in project
// Map<String, String> superClassPackages = new HashMap<String, String>();
// for (PythonTreeClass superClass : this.getModelGeneralizationsInProject(modelClass)) {
//
// // get package structure, e.g. mypackage.subpackage.MyClass
// String packageDef = "";
// PythonTreePackage superPackage = superClass.getParent();
// while (superPackage != null && (! (superPackage instanceof PythonTreeRoot))) {
// packageDef = superPackage.getName() + "." + packageDef;
// superPackage = superPackage.getParent();
// }
// String superClassModuleName = superClass.inFile.getName().replace(".py", "");
// packageDef += superClassModuleName;
// superClassPackages.put(superClass.getName(), packageDef);
// }
//
// // find import statements in code
// // suppose, that all import statements are at the beginning of the
// // file, only white or comment lines can be above them.
// String[] fileContent = this.inFile.getFileContent().split("\n");
// int lineNo = 0;
// String line = "";
// if (superClassPackages.size() > 0) {
// do {
// line = fileContent[lineNo];
// String[] importParts = getImport(line);
// if (importParts != null){
// String className = importParts[0];
// String packageDef = importParts[1];
// String comment = importParts[2];
// // if class is imported, but package changed -> rewrite line!
// // FIXME: This is messed up since an import may be done without a "from" statement;
// // it might look like "import mypackage" and refer to it by "mypackage.MyClass"
// // or even like "import mypackage.mysubpackage as myalias"...
// // therefore I disable import fixing as long as this issue has not been solved
// // by Jakob
// if (superClassPackages.containsKey(className) &&
// ( ! packageDef.equals(superClassPackages.get(className)))) {
// String newString = "from " + superClassPackages.get(className) + " import " + className + comment;
// FileRefactoring.replaceLine(this.inFile, lineNo, newString, true);
// this.setChanged(null, lineNo);
// return true;
// }
// if (superClassPackages.containsKey(className) && superClassPackages.get(className).equals(packageDef)) {
// // line is OK -> remove them from new-import-to-insert-list
// superClassPackages.remove(className);
// }
// }
// lineNo ++;
// } while(line.matches("[\\s]*[#]?") || line.matches("from.*import.*")||line.matches("import.*"));
//
// // now, all lines were analyzed; the needed import, that were not covered
// // by a line, must be inserted now!
// if (superClassPackages.size() == 0)
// return false;
//
// String insertLines = "";
// for (String superClassName : superClassPackages.keySet()) {
// String packageLine = superClassPackages.get(superClassName);
// insertLines += "from " + packageLine + " import " + superClassName + "\n";
// }
// // if last line is empty, don't insert new empty line
// if (lineNo == 1)
// // no empty lines are in fron of the insertion -> insert empty line
// insertLines += "\n";
// else if (line.length() > 0 && (! fileContent[lineNo-2].matches("^[\\s]*$"))) {
// insertLines += "\n";
// }
// FileRefactoring.insertAtLine(this.inFile, lineNo-1, insertLines);
// this.setChanged(null, lineNo);
// return true;
// }
//
// return false;
}
/**
* Returns a string array containing the class name, package
* definition and the trailing comment at the zero, first and second
* indices for the given import line.
*
* @param line The python import line
*
* @return A string array containing class name and packege definition
* or null if not found.
*
* @see PythonTreeClass#FROM_IMPORT_PATTERN
*/
private String[] getImport(String line) {
Matcher matcher = FROM_IMPORT_PATTERN.matcher(line);
if (matcher.find()) {
String[] importParts = new String[3];
importParts[0] = matcher.group(2);
importParts[1] = matcher.group(1);
importParts[2] = matcher.group(3);
return importParts;
}
// find import statements in code
// suppose, that all import statements are at the beginning of the
// file, only white or comment lines can be above them.
String[] fileContent = this.inFile.getFileContent().split("\n");
int lineNo = 0;
String line = "";
if (superClassPackages.size() > 0) {
do {
line = fileContent[lineNo];
Pattern pattern = Pattern.compile("[\\s]*from[\\s]*([^\\s]*)[\\s]*import[\\s]*([^\\s#]*)([\\s#]?.*)");
Matcher matcher = pattern.matcher(line);
if (matcher.find()){
String className = matcher.group(2);
String packageDef = matcher.group(1);
// if class is imported, but package changed -> rewrite line!
if (superClassPackages.containsKey(className) &&
( ! packageDef.equals(superClassPackages.get(className)))) {
String newString = "from " + superClassPackages.get(className) + " import " + className + matcher.group(3);
FileRefactoring.replaceLine(this.inFile, lineNo, newString, true);
this.setChanged(null, lineNo);
return true;
}
if (superClassPackages.containsKey(className) && superClassPackages.get(className).equals(packageDef)) {
// line is OK -> remove them from new-import-to-insert-list
superClassPackages.remove(className);
}
}
lineNo ++;
} while(line.matches("[\\s]*[#]?") || line.matches("from.*import.*")||line.matches("import.*"));
// now, all lines were analyzed; the needed import, that were not covered
// by a line, must be inserted now!
if (superClassPackages.size() == 0)
return false;
String insertLines = "";
for (String superClassName : superClassPackages.keySet()) {
String packageLine = superClassPackages.get(superClassName);
insertLines += "from " + packageLine + " import " + superClassName + "\n";
}
// if last line is empty, don't insert new empty line
if (lineNo == 1)
// no empty lines are in fron of the insertion -> insert empty line
insertLines += "\n";
else if (line.length() > 0 && (! fileContent[lineNo-2].matches("^[\\s]*$"))) {
insertLines += "\n";
}
FileRefactoring.insertAtLine(this.inFile, lineNo-1, insertLines);
this.setChanged(null, lineNo);
return true;
}
return false;
return null;
}
/**
@ -1163,23 +1213,101 @@ public class PythonTreeClass extends PythonTreeNode{
/**
* Gets a complete package structure (including class)
* from the class imports for a class (separated by "/"
* e.g. "a/b/c/MyClass" for "MyClass", if there is
* e.g. "/a/b/c/MyClass" for "MyClass", if there is
* an import line "from a.b.c.MyClassFile import MyClass"
* @param className the name of the class
* @return the full package/class structure
* or "import a.b.c.MyClassFile" and class name is "MyClassFile.MyClass"
* or "import a.b.c.MyClassFile as myalias" and class name is "myalias.MyClass".
*
* @param className The name of the class too lookup
* @return The full package / class structure
*/
public String getPackFromImports(String className) {
String fileContent = this.getInFile().getFileContent();
Pattern pattern = Pattern.compile(".*from[\\s]+([^\\s]+)\\.[^\\.\\s]+[\\s]+import[\\s]+"+className+".*");
Matcher matcher = pattern.matcher(fileContent);
// if no import (with class structure in front) was found, we assume that the class
// is in the same package
if (! matcher.find()){
return this.getPackageStructure() + className;
search:
if (!packageCache.containsKey(className)) {
// the fully qualified package name in the format "/mypackage/mysubpackage/MyClass"
String packageName = null;
String fileContent = this.getInFile().getFileContent();
Pattern pattern = Pattern.compile(".*from[\\s]+([^\\s]+)\\.[^\\.\\s]+[\\s]+import[\\s]+" + className + ".*");
Matcher matcher = pattern.matcher(fileContent);
if (matcher.find()){
packageName = "/" + (matcher.group(1) + "." + className).replace(".", "/");
packageCache.put(className, packageName);
break search;
}
// lets check for fully qualified package imports
// e.g. "import mypackage.myclassfile" and "mypackage.myclassfile.MyClass"
// or aliases "import mypackage.mysubpackage.myclassfile as myalias" and "myalias.MyClass"
String[] parts = className.split("\\.");
if (parts.length > 1) { // there is at least one package, without the classfile
String strippedClassName = parts[parts.length - 1];
String prefix = join(parts, ".", 0, parts.length - 1);
pattern = Pattern.compile(".*import[\\s]+" + prefix + ".*");
matcher = pattern.matcher(fileContent);
if (matcher.find()) { // we have a full qualified import
packageName = "/" + join(parts, "/", 0 , 2) + "/" + strippedClassName;
packageCache.put(className, packageName);
break search;
}
pattern = Pattern.compile(".*import[\\s]+([^\\s]+)\\.[^\\.\\s]+[\\s]+as[\\s]+" + prefix + ".*");
matcher = pattern.matcher(fileContent);
if (matcher.find()) { // we have an alias
packageName = "/" + (matcher.group(1) + "." + strippedClassName).replace(".", "/");
packageCache.put(className, packageName);
break search;
}
}
// if no import (with class structure in front) was found, we assume that the class
// is in the same package
packageCache.put(className, this.getPackageStructure() + className);
}
String result = "/" + (matcher.group(1) + "." + className).replace(".", "/");
return result;
return packageCache.get(className);
}
/**
* Utility method to create a string from a string array,
* joined with the given separator.
*
* @param parts The string array to get the parts to join from
* @param separator The string that glues the parts together
* @return The joined string from the given parts and separator
*
* @see PythonTreeClass#join(String[], String, int, int)
*/
private String join(String[] parts, String separator) {
return join(parts, separator, 0, parts.length);
}
/**
* Utility method to create a string from a (partial) string array,
* joined with the given separator.
* <br />
* E.g.:
* <code>
* String abc = join(new String[] { a, b, c }, "/", 0, 3);
* </code>
* will result in
* <code>
* "a/b/c"
* </code>
*
* @param parts The string array to get the parts to join from
* @param separator The string that glues the parts together
* @param offset From where to start in the parts array
* @param length How many elements from offset should be joined
* @return The joined string from the given parts and separator
*/
private String join(String[] parts, String separator, int offset, int length) {
if (parts.length - offset < length) {
return null;
}
StringBuffer buffer = new StringBuffer();
for (int i = offset; i < length; i++) {
buffer.append(parts[i]);
if (i < length - 1) {
buffer.append(separator);
}
}
return buffer.toString();
}
/**