show class quick doc if constructor isn't documented (part of PY-381)

This commit is contained in:
Dmitry Jemerov
2010-02-01 21:17:48 +03:00
parent 143e26693e
commit 67d486ebcd
11 changed files with 128 additions and 111 deletions
@@ -13,8 +13,8 @@ import org.jetbrains.annotations.Nullable;
* User: dcheryasov
* Date: Jun 7, 2009 5:06:12 AM
*/
public class PythonDosStringFinder {
private PythonDosStringFinder() {}
public class PythonDocStringFinder {
private PythonDocStringFinder() {}
/**
* Looks for a doc string under given parent.
@@ -166,7 +166,43 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
cat.add(prolog_cat).addWith(TagCode, doc_cat).add(epilog_cat); // pre-assemble; then add stuff to individual cats as needed
cat = wrapInTag("html", wrapInTag("body", cat));
element = resolveToDocStringOwner(element, originalElement, prolog_cat);
// now element may contain a doc string
if (element instanceof PyDocStringOwner) {
String docString = null;
PyStringLiteralExpression doc_expr = ((PyDocStringOwner) element).getDocStringExpression();
if (doc_expr != null) docString = doc_expr.getStringValue();
// doc of what?
if (element instanceof PyClass) {
PyClass cls = (PyClass)element;
doc_cat.addWith(TagSmall, describeClass(cls, TagBold));
}
else if (element instanceof PyFunction) {
PyFunction fun = (PyFunction)element;
PyClass cls = fun.getContainingClass();
if (cls != null) doc_cat.addWith(TagSmall, $("class ", cls.getName(), BR));
doc_cat.add(describeFunction(fun, TagItalic, BR, TagBold, LCombUp));
if (docString == null) {
addInheritedDocString(fun, cls, doc_cat, epilog_cat);
}
}
else if (element instanceof PyFile) {
// what to prepend to a module description??
}
else { // not a func, not a class
doc_cat.add(combUp(PyUtil.getReadableRepr(element, false)));
}
if (docString != null) {
doc_cat.add(BR).add(combUpDocString(docString));
}
else if (prolog_cat.isEmpty() && doc_cat.isEmpty() && epilog_cat.isEmpty()) return null; // got nothing to say!
return cat.toString();
}
return null;
}
private static PsiElement resolveToDocStringOwner(PsiElement element, PsiElement originalElement, ChainIterable<String> prolog_cat) {
// here the ^Q target is already resolved; the resolved element may point to intermediate assignments
boolean reassignment_marked = false;
if (element instanceof PyTargetExpression) {
@@ -199,84 +235,66 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
}
}
// now element may contain a doc string
if (element instanceof PyDocStringOwner) {
String docString = null;
PyStringLiteralExpression doc_expr = ((PyDocStringOwner) element).getDocStringExpression();
if (doc_expr != null) docString = doc_expr.getStringValue();
// doc of what?
if (element instanceof PyClass) {
PyClass cls = (PyClass)element;
doc_cat.addWith(TagSmall, describeClass(cls, TagBold));
if (element instanceof PyFunction && PyNames.INIT.equals(((PyFunction)element).getName())) {
final PyStringLiteralExpression expression = ((PyFunction)element).getDocStringExpression();
if (expression == null) {
PyClass containingClass = ((PyFunction) element).getContainingClass();
if (containingClass != null) {
element = containingClass;
}
}
else if (element instanceof PyFunction) {
PyFunction fun = (PyFunction)element;
PyClass cls = fun.getContainingClass();
if (cls != null) doc_cat.addWith(TagSmall, $("class ", cls.getName(), BR));
doc_cat.add(describeFunction(fun, TagItalic, BR, TagBold, LCombUp));
boolean not_found = true;
if (docString == null) {
String meth_name = fun.getName();
if (cls != null && meth_name != null ) {
// look for inherited and its doc
for (PyClass ancestor : cls.iterateAncestors()) {
PyFunction inherited = ancestor.findMethodByName(meth_name, false);
if (inherited != null) {
PyStringLiteralExpression doc_elt = inherited.getDocStringExpression();
if (doc_elt != null) {
String inherited_doc = doc_elt.getStringValue();
if (inherited_doc.length() > 1) {
epilog_cat
.add(BR).add(BR)
.add(PyBundle.message("QDOC.copied.from.$0.$1", ancestor.getName(), meth_name))
.add(BR).add(BR)
.addWith(TagCode, $(inherited_doc))
;
not_found = false;
break;
}
}
}
}
}
return element;
}
if (not_found) {
// above could have not worked because inheritance is not searched down to 'object'.
// for well-known methods, copy built-in doc string.
// TODO: also handle predefined __xxx__ that are not part of 'object'.
if (PyNames.UnderscoredAttributes.contains(meth_name)) {
PyClassType objtype = PyBuiltinCache.getInstance(fun).getObjectType(); // old- and new-style classes share the __xxx__ stuff
if (objtype != null) {
PyClass objcls = objtype.getPyClass();
if (objcls != null) {
PyFunction obj_underscored = objcls.findMethodByName(meth_name, false);
if (obj_underscored != null) {
PyStringLiteralExpression predefined_doc_expr = obj_underscored.getDocStringExpression();
String predefined_doc = predefined_doc_expr != null? predefined_doc_expr.getStringValue() : null;
if (predefined_doc != null && predefined_doc.length() > 1) { // only a real-looking doc string counts
doc_cat.add(predefined_doc);
epilog_cat.add(BR).add(BR).add(PyBundle.message("QDOC.copied.from.builtin"));
}
}
}
private static void addInheritedDocString(PyFunction fun, PyClass cls, ChainIterable<String> doc_cat, ChainIterable<String> epilog_cat) {
boolean not_found = true;
String meth_name = fun.getName();
if (cls != null && meth_name != null ) {
// look for inherited and its doc
for (PyClass ancestor : cls.iterateAncestors()) {
PyFunction inherited = ancestor.findMethodByName(meth_name, false);
if (inherited != null) {
PyStringLiteralExpression doc_elt = inherited.getDocStringExpression();
if (doc_elt != null) {
String inherited_doc = doc_elt.getStringValue();
if (inherited_doc.length() > 1) {
epilog_cat
.add(BR).add(BR)
.add(PyBundle.message("QDOC.copied.from.$0.$1", ancestor.getName(), meth_name))
.add(BR).add(BR)
.addWith(TagCode, $(inherited_doc))
;
not_found = false;
break;
}
}
}
}
if (not_found) {
// above could have not worked because inheritance is not searched down to 'object'.
// for well-known methods, copy built-in doc string.
// TODO: also handle predefined __xxx__ that are not part of 'object'.
if (PyNames.UnderscoredAttributes.contains(meth_name)) {
PyClassType objtype = PyBuiltinCache.getInstance(fun).getObjectType(); // old- and new-style classes share the __xxx__ stuff
if (objtype != null) {
PyClass objcls = objtype.getPyClass();
if (objcls != null) {
PyFunction obj_underscored = objcls.findMethodByName(meth_name, false);
if (obj_underscored != null) {
PyStringLiteralExpression predefined_doc_expr = obj_underscored.getDocStringExpression();
String predefined_doc = predefined_doc_expr != null? predefined_doc_expr.getStringValue() : null;
if (predefined_doc != null && predefined_doc.length() > 1) { // only a real-looking doc string counts
doc_cat.add(predefined_doc);
epilog_cat.add(BR).add(BR).add(PyBundle.message("QDOC.copied.from.builtin"));
}
}
}
}
}
}
else if (element instanceof PyFile) {
// what to prepend to a module description??
}
else { // not a func, not a class
doc_cat.add(combUp(PyUtil.getReadableRepr(element, false)));
}
if (docString != null) {
doc_cat.add(BR).add(combUpDocString(docString));
}
else if (prolog_cat.isEmpty() && doc_cat.isEmpty() && epilog_cat.isEmpty()) return null; // got nothing to say!
return cat.toString();
}
return null;
}
private static FP.Lambda1<String, String> LCombUp = new FP.Lambda1<String, String>() {
@@ -332,22 +350,6 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
}
};
/*
private static StringBuilder join(String delimiter, Iterable list, StringBuilder cat) {
boolean is_next = false;
for (Object item : list) {
if (is_next) cat.append(delimiter);
else is_next = true;
cat.append(item.toString());
}
return cat;
}
private static String join(String delimiter, List list) {
return join(delimiter, list, new StringBuilder()).toString();
}
*/
private static <T> Iterable<T> interleave(Iterable<T> source, T filler) {
List<T> ret = new LinkedList<T>();
boolean is_next = false;
@@ -7,7 +7,7 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PythonDosStringFinder;
import com.jetbrains.python.PythonDocStringFinder;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.Nullable;
@@ -40,7 +40,7 @@ class AddImportHelper {
}
// maybe we arrived at the doc comment stmt; skip over it, too
else if (!skipped_over_imports && ! skipped_over_doc && file instanceof PyFile) {
PsiElement doc_elt = PythonDosStringFinder.find((PyElement)file); // this gives the literal; its parent is the expr seeker may have encountered
PsiElement doc_elt = PythonDocStringFinder.find((PyElement)file); // this gives the literal; its parent is the expr seeker may have encountered
if (doc_elt != null && doc_elt.getParent() == seeker) {
feeler = seeker.getNextSibling();
seeker = feeler; // skip over doc even if there's nothing below it
@@ -12,7 +12,7 @@ import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonDosStringFinder;
import com.jetbrains.python.PythonDocStringFinder;
import com.jetbrains.python.codeInsight.dataflow.scope.Scope;
import com.jetbrains.python.codeInsight.dataflow.scope.impl.ScopeImpl;
import com.jetbrains.python.psi.*;
@@ -285,7 +285,7 @@ public class PyClassImpl extends PyPresentableElementImpl<PyClassStub> implement
}
public PyStringLiteralExpression getDocStringExpression() {
return PythonDosStringFinder.find(getStatementList());
return PythonDocStringFinder.find(getStatementList());
}
public String toString() {
@@ -14,7 +14,7 @@ import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PythonDosStringFinder;
import com.jetbrains.python.PythonDocStringFinder;
import com.jetbrains.python.PythonFileType;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.codeInsight.dataflow.scope.Scope;
@@ -250,7 +250,7 @@ public class PyFileImpl extends PsiFileBase implements PyFile, PyExpression {
}
public PyStringLiteralExpression getDocStringExpression() {
return PythonDosStringFinder.find(this);
return PythonDocStringFinder.find(this);
}
public void subtreeChanged() {
@@ -27,11 +27,11 @@ import com.intellij.util.Icons;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonDosStringFinder;
import com.jetbrains.python.PythonDocStringFinder;
import com.jetbrains.python.codeInsight.controlflow.PyControlFlowBuilder;
import com.jetbrains.python.codeInsight.dataflow.scope.Scope;
import com.jetbrains.python.codeInsight.dataflow.scope.impl.ScopeImpl;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.codeInsight.controlflow.PyControlFlowBuilder;
import com.jetbrains.python.psi.stubs.PyClassStub;
import com.jetbrains.python.psi.stubs.PyFunctionStub;
import com.jetbrains.python.toolbox.SingleIterable;
@@ -149,7 +149,7 @@ public class PyFunctionImpl extends PyPresentableElementImpl<PyFunctionStub> imp
}
public PyStringLiteralExpression getDocStringExpression() {
return PythonDosStringFinder.find(getStatementList());
return PythonDocStringFinder.find(getStatementList());
}
protected String getElementLocation() {
@@ -18,7 +18,7 @@ package com.jetbrains.python.validation;
import com.intellij.lang.annotation.Annotation;
import com.jetbrains.python.PyHighlighter;
import com.jetbrains.python.PythonDosStringFinder;
import com.jetbrains.python.PythonDocStringFinder;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PyFunction;
@@ -31,18 +31,18 @@ public class DocStringAnnotator extends PyAnnotator {
@Override
public void visitPyFile(PyFile node) {
annotateDocStringStmt(PythonDosStringFinder.find(node));
annotateDocStringStmt(PythonDocStringFinder.find(node));
}
@Override
public void visitPyFunction(PyFunction node) {
annotateDocStringStmt(PythonDosStringFinder.find(node.getStatementList()));
annotateDocStringStmt(PythonDocStringFinder.find(node.getStatementList()));
}
@Override
public void visitPyClass(PyClass node) {
annotateDocStringStmt(PythonDosStringFinder.find(node.getStatementList()));
annotateDocStringStmt(PythonDocStringFinder.find(node.getStatementList()));
}
private void annotateDocStringStmt(PyStringLiteralExpression stmt) {
@@ -0,0 +1 @@
<html><body><code><small>class <b>Foo</b>(object)</small><br>Doc&nbsp;of&nbsp;Foo.</code></body></html>
@@ -0,0 +1,7 @@
# direct class doc
class Foo(object):
"<the_doc>Doc of Foo."
def __init__(self):
pass
<the_ref>Foo()
@@ -28,21 +28,22 @@ public class PyQuickDocTest extends LightMarkedTestCase {
myProvider = new PythonDocumentationProvider();
}
protected String getTestDataPath() {
return PythonTestUtil.getTestDataPath() + "/quickdoc/";
}
private void checkByHTML(String text) throws Exception {
assertNotNull(text);
String filePath = getTestName(false) + ".html";
String filePath = "/quickdoc/" + getTestName(false) + ".html";
final String fullPath = getTestDataPath() + filePath;
final VirtualFile vFile = LocalFileSystem.getInstance().findFileByPath(fullPath.replace(File.separatorChar, '/'));
assertNotNull("file " + filePath + " not found", vFile);
assertNotNull("file " + fullPath + " not found", vFile);
String fileText = StringUtil.convertLineSeparators(VfsUtil.loadText(vFile), "\n");
assertEquals(fileText.trim(), text.trim());
}
@Override
protected Map<String, PsiElement> loadTest() throws Exception {
return configureByFile("/quickdoc/" + getTestName(false) + ".py");
}
private void processRefDocPair() throws Exception {
Map<String, PsiElement> marks = loadTest();
assertEquals(2, marks.size());
@@ -53,7 +54,7 @@ public class PyQuickDocTest extends LightMarkedTestCase {
PsiElement ref_elt = marks.get("<the_ref>").getParent(); // ident -> expr
final PyDocStringOwner doc_owner = (PyDocStringOwner)((PyReferenceExpression)ref_elt).resolve();
assertEquals(doc_owner.getDocStringExpression(), doc_elt);
assertEquals(doc_elt, doc_owner.getDocStringExpression());
checkByHTML(myProvider.generateDoc(doc_owner, null));
}
@@ -70,6 +71,13 @@ public class PyQuickDocTest extends LightMarkedTestCase {
processRefDocPair();
}
public void testClassUndocumentedConstructor() throws Exception {
Map<String, PsiElement> marks = loadTest();
PsiElement ref_elt = marks.get("<the_ref>").getParent(); // ident -> expr
final PyDocStringOwner doc_owner = (PyDocStringOwner)((PyReferenceExpression)ref_elt).resolve();
checkByHTML(myProvider.generateDoc(doc_owner, null));
}
public void testCallFunc() throws Exception {
processRefDocPair();
}
@@ -7,6 +7,7 @@ import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.TestDataFile;
import com.intellij.util.containers.HashMap;
import org.jetbrains.annotations.NonNls;
@@ -47,12 +48,12 @@ public abstract class LightMarkedTestCase extends PyLightFixtureTestCase {
* @return a mapping of markers to PSI elements
* @throws Exception
*/
protected Map<String, PsiElement> configureByFile(@NonNls String filePath, @NonNls String markerRegexp)
protected Map<String, PsiElement> configureByFile(@TestDataFile @NonNls String filePath, @NonNls String markerRegexp)
throws Exception
{
final String fullPath = getTestDataPath() + filePath;
final VirtualFile vFile = LocalFileSystem.getInstance().findFileByPath(fullPath.replace(File.separatorChar, '/'));
assertNotNull("file " + filePath + " not found", vFile);
assertNotNull("file " + fullPath + " not found", vFile);
String fileText = StringUtil.convertLineSeparators(VfsUtil.loadText(vFile), "\n");
@@ -111,6 +112,4 @@ public abstract class LightMarkedTestCase extends PyLightFixtureTestCase {
String fname = getTestName(false) + ".py";
return configureByFile(fname);
}
protected abstract String getTestDataPath();
}