blob: 4c3432c7e84aa4c8424630e771771538ef51e3a7 [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.documentation;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonFileType;
import com.jetbrains.python.console.PyConsoleUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyCallExpressionHelper;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
import com.jetbrains.python.psi.resolve.RootVisitor;
import com.jetbrains.python.psi.resolve.RootVisitorHost;
import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.toolbox.ChainIterable;
import com.jetbrains.python.toolbox.Maybe;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.jetbrains.python.documentation.DocumentationBuilderKit.*;
class PyDocumentationBuilder {
private final PsiElement myElement;
private final PsiElement myOriginalElement;
private ChainIterable<String> myResult;
private ChainIterable<String> myProlog; // sequence for reassignment info, etc
private ChainIterable<String> myBody; // sequence for doc string
private ChainIterable<String> myEpilog; // sequence for doc "copied from" notices and such
private static final Pattern ourSpacesPattern = Pattern.compile("^\\s+");
public PyDocumentationBuilder(PsiElement element, PsiElement originalElement) {
myElement = element;
myOriginalElement = originalElement;
myResult = new ChainIterable<String>();
myProlog = new ChainIterable<String>();
myBody = new ChainIterable<String>();
myEpilog = new ChainIterable<String>();
myResult.add(myProlog).addWith(TagCode, myBody).add(myEpilog); // pre-assemble; then add stuff to individual cats as needed
myResult = wrapInTag("html", wrapInTag("body", myResult));
}
public String build() {
final ChainIterable<String> reassignCat = new ChainIterable<String>(); // sequence for reassignment info, etc
PsiElement followed = resolveToDocStringOwner(reassignCat);
// check if we got a property ref.
// if so, element is an accessor, and originalElement if an identifier
// TODO: use messages from resources!
PyClass cls;
PsiElement outer = null;
boolean is_property = false;
String accessor_kind = "None";
final TypeEvalContext context = TypeEvalContext.userInitiated(myElement.getContainingFile());
if (myOriginalElement != null) {
String elementName = myOriginalElement.getText();
if (PyUtil.isPythonIdentifier(elementName)) {
outer = myOriginalElement.getParent();
if (outer instanceof PyQualifiedExpression) {
PyExpression qual = ((PyQualifiedExpression)outer).getQualifier();
if (qual != null) {
PyType type = context.getType(qual);
if (type instanceof PyClassType) {
cls = ((PyClassType)type).getPyClass();
Property property = cls.findProperty(elementName, true);
if (property != null) {
is_property = true;
final AccessDirection dir = AccessDirection.of((PyElement)outer);
Maybe<Callable> accessor = property.getByDirection(dir);
myProlog
.addItem("property ").addWith(TagBold, $().addWith(TagCode, $(elementName)))
.addItem(" of ").add(PythonDocumentationProvider.describeClass(cls, TagCode, true, true))
;
if (accessor.isDefined() && property.getDoc() != null) {
myBody.addItem(": ").addItem(property.getDoc()).addItem(BR);
}
else {
final Callable getter = property.getGetter().valueOrNull();
if (getter != null && getter != myElement && getter instanceof PyFunction) {
// not in getter, getter's doc comment may be useful
PyStringLiteralExpression docstring = ((PyFunction)getter).getDocStringExpression();
if (docstring != null) {
myProlog
.addItem(BR).addWith(TagItalic, $("Copied from getter:")).addItem(BR)
.addItem(docstring.getStringValue())
;
}
}
myBody.addItem(BR);
}
myBody.addItem(BR);
if (accessor.isDefined() && accessor.value() == null) followed = null;
if (dir == AccessDirection.READ) {
accessor_kind = "Getter";
}
else if (dir == AccessDirection.WRITE) {
accessor_kind = "Setter";
}
else {
accessor_kind = "Deleter";
}
if (followed != null) myEpilog.addWith(TagSmall, $(BR, BR, accessor_kind, " of property")).addItem(BR);
}
}
}
}
}
}
if (myProlog.isEmpty() && !is_property && !isAttribute()) {
myProlog.add(reassignCat);
}
// now followed may contain a doc string
if (followed instanceof PyDocStringOwner) {
String docString = null;
PyStringLiteralExpression doc_expr = ((PyDocStringOwner)followed).getDocStringExpression();
if (doc_expr != null) docString = doc_expr.getStringValue();
// doc of what?
if (followed instanceof PyClass) {
cls = (PyClass)followed;
myBody.add(PythonDocumentationProvider.describeDecorators(cls, TagItalic, BR, LCombUp));
myBody.add(PythonDocumentationProvider.describeClass(cls, TagBold, true, false));
}
else if (followed instanceof PyFunction) {
PyFunction fun = (PyFunction)followed;
if (!is_property) {
cls = fun.getContainingClass();
if (cls != null) {
myBody.addWith(TagSmall, PythonDocumentationProvider.describeClass(cls, TagCode, true, true)).addItem(BR).addItem(BR);
}
}
else {
cls = null;
}
myBody
.add(PythonDocumentationProvider.describeDecorators(fun, TagItalic, BR, LCombUp))
.add(PythonDocumentationProvider.describeFunction(fun, TagBold, LCombUp));
if (docString == null) {
addInheritedDocString(fun, cls);
}
}
else if (followed instanceof PyFile) {
addModulePath((PyFile)followed);
}
if (docString != null) {
myBody.addItem(BR);
addFormattedDocString(myElement, docString, myBody, myEpilog);
}
}
else if (is_property) {
// if it was a normal accessor, ti would be a function, handled by previous branch
String accessor_message;
if (followed != null) {
accessor_message = "Declaration: ";
}
else {
accessor_message = accessor_kind + " is not defined.";
}
myBody.addWith(TagItalic, $(accessor_message)).addItem(BR);
if (followed != null) myBody.addItem(combUp(PyUtil.getReadableRepr(followed, false)));
}
else if (isAttribute()) {
addAttributeDoc();
}
else if (followed instanceof PyNamedParameter) {
myBody.addItem(combUp("Parameter " + PyUtil.getReadableRepr(followed, false)));
boolean typeFromDocstringAdded = addTypeAndDescriptionFromDocstring((PyNamedParameter)followed);
if (outer instanceof PyExpression) {
PyType type = context.getType((PyExpression)outer);
if (type != null) {
String s = null;
if (type instanceof PyDynamicallyEvaluatedType) {
if (!typeFromDocstringAdded) {
//don't add dynamic type if docstring type specified
s = "\nDynamically inferred type: ";
}
}
else {
if (outer.getReference() != null) {
PsiElement target = outer.getReference().resolve();
if (target instanceof PyTargetExpression &&
((PyTargetExpression)target).getName().equals(((PyNamedParameter)followed).getName())) {
s = "\nReassigned value has type: ";
}
}
}
if (s == null && !typeFromDocstringAdded) {
s = "\nInferred type: ";
}
if (s != null) {
myBody
.addItem(combUp(s));
PythonDocumentationProvider.describeTypeWithLinks(myBody, followed, type, context);
}
}
}
}
else if (followed != null && outer instanceof PyReferenceExpression) {
myBody.addItem(combUp("\nInferred type: "));
PythonDocumentationProvider.describeExpressionTypeWithLinks(myBody, (PyReferenceExpression)outer, context);
}
if (myBody.isEmpty() && myEpilog.isEmpty()) {
return null; // got nothing substantial to say!
}
else {
return myResult.toString();
}
}
private boolean isAttribute() {
return myElement instanceof PyTargetExpression && PyUtil.isAttribute((PyTargetExpression)myElement);
}
@Nullable
private PsiElement resolveToDocStringOwner(ChainIterable<String> prolog_cat) {
// here the ^Q target is already resolved; the resolved element may point to intermediate assignments
if (myElement instanceof PyTargetExpression) {
final String target_name = myElement.getText();
//prolog_cat.add(TagSmall.apply($("Assigned to ", element.getText(), BR)));
prolog_cat.addWith(TagSmall, $(PyBundle.message("QDOC.assigned.to.$0", target_name)).addItem(BR));
final PyExpression assignedValue = ((PyTargetExpression)myElement).findAssignedValue();
if (assignedValue instanceof PyReferenceExpression) {
final PsiElement resolved = resolveWithoutImplicits((PyReferenceExpression)assignedValue);
if (resolved != null) {
return resolved;
}
}
return assignedValue;
}
if (myElement instanceof PyReferenceExpression) {
//prolog_cat.add(TagSmall.apply($("Assigned to ", element.getText(), BR)));
prolog_cat.addWith(TagSmall, $(PyBundle.message("QDOC.assigned.to.$0", myElement.getText())).addItem(BR));
return resolveWithoutImplicits((PyReferenceExpression)myElement);
}
// it may be a call to a standard wrapper
if (myElement instanceof PyCallExpression) {
final PyCallExpression call = (PyCallExpression)myElement;
Pair<String, PyFunction> wrap_info = PyCallExpressionHelper.interpretAsModifierWrappingCall(call, myOriginalElement);
if (wrap_info != null) {
String wrapper_name = wrap_info.getFirst();
PyFunction wrapped_func = wrap_info.getSecond();
//prolog_cat.addWith(TagSmall, $("Wrapped in ").addWith(TagCode, $(wrapper_name)).add(BR));
prolog_cat.addWith(TagSmall, $(PyBundle.message("QDOC.wrapped.in.$0", wrapper_name)).addItem(BR));
return wrapped_func;
}
}
return myElement;
}
private static PsiElement resolveWithoutImplicits(final PyReferenceExpression element) {
final QualifiedResolveResult resolveResult = element.followAssignmentsChain(PyResolveContext.noImplicits());
return resolveResult.isImplicit() ? null : resolveResult.getElement();
}
private void addInheritedDocString(PyFunction fun, PyClass cls) {
boolean not_found = true;
String meth_name = fun.getName();
if (cls != null && meth_name != null) {
final boolean is_constructor = PyNames.INIT.equals(meth_name);
// look for inherited and its doc
Iterable<PyClass> classes = cls.getAncestorClasses();
if (is_constructor) {
// look at our own class again and maybe inherit class's doc
classes = new ChainIterable<PyClass>(cls).add(classes);
}
for (PyClass ancestor : classes) {
PyStringLiteralExpression doc_elt = null;
PyFunction inherited = null;
boolean is_from_class = false;
if (is_constructor) doc_elt = cls.getDocStringExpression();
if (doc_elt != null) {
is_from_class = true;
}
else {
inherited = ancestor.findMethodByName(meth_name, false);
}
if (inherited != null) {
doc_elt = inherited.getDocStringExpression();
}
if (doc_elt != null) {
String inherited_doc = doc_elt.getStringValue();
if (inherited_doc.length() > 1) {
myEpilog.addItem(BR).addItem(BR);
String ancestor_name = ancestor.getName();
String marker = (cls == ancestor) ? PythonDocumentationProvider.LINK_TYPE_CLASS : PythonDocumentationProvider.LINK_TYPE_PARENT;
final String ancestor_link =
$().addWith(new DocumentationBuilderKit.LinkWrapper(marker + ancestor_name), $(ancestor_name)).toString();
if (is_from_class) {
myEpilog.addItem(PyBundle.message("QDOC.copied.from.class.$0", ancestor_link));
}
else {
myEpilog.addItem(PyBundle.message("QDOC.copied.from.$0.$1", ancestor_link, meth_name));
}
myEpilog.addItem(BR).addItem(BR);
ChainIterable<String> formatted = new ChainIterable<String>();
ChainIterable<String> unformatted = new ChainIterable<String>();
addFormattedDocString(fun, inherited_doc, formatted, unformatted);
myEpilog.addWith(TagCode, formatted).add(unformatted);
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)) {
addPredefinedMethodDoc(fun, meth_name);
}
}
}
}
private void addPredefinedMethodDoc(PyFunction fun, String meth_name) {
PyClassType objtype = PyBuiltinCache.getInstance(fun).getObjectType(); // old- and new-style classes share the __xxx__ stuff
if (objtype != null) {
PyClass objcls = objtype.getPyClass();
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
addFormattedDocString(fun, predefined_doc, myBody, myBody);
myEpilog.addItem(BR).addItem(BR).addItem(PyBundle.message("QDOC.copied.from.builtin"));
}
}
}
}
private static void addFormattedDocString(PsiElement element, @NotNull String docstring,
ChainIterable<String> formattedOutput, ChainIterable<String> unformattedOutput) {
final Project project = element.getProject();
List<String> formatted = PyStructuredDocstringFormatter.formatDocstring(element, docstring);
if (formatted != null) {
unformattedOutput.add(formatted);
return;
}
boolean isFirstLine;
final List<String> result = new ArrayList<String>();
String[] lines = removeCommonIndentation(docstring);
// reconstruct back, dropping first empty fragment as needed
isFirstLine = true;
int tabSize = CodeStyleSettingsManager.getSettings(project).getTabSize(PythonFileType.INSTANCE);
for (String line : lines) {
if (isFirstLine && ourSpacesPattern.matcher(line).matches()) continue; // ignore all initial whitespace
if (isFirstLine) {
isFirstLine = false;
}
else {
result.add(BR);
}
int leadingTabs = 0;
while (leadingTabs < line.length() && line.charAt(leadingTabs) == '\t') {
leadingTabs++;
}
if (leadingTabs > 0) {
line = StringUtil.repeatSymbol(' ', tabSize * leadingTabs) + line.substring(leadingTabs);
}
result.add(combUp(line));
}
formattedOutput.add(result);
}
/**
* Adds type and description representation from function docstring
*
* @param parameter parameter of a function
* @return true if type from docstring was added
*/
private boolean addTypeAndDescriptionFromDocstring(@NotNull PyNamedParameter parameter) {
PyFunction function = PsiTreeUtil.getParentOfType(parameter, PyFunction.class);
if (function != null) {
final String docString = PyPsiUtils.strValue(function.getDocStringExpression());
Pair<String, String> typeAndDescr = getTypeAndDescr(docString, parameter);
String type = typeAndDescr.first;
String desc = typeAndDescr.second;
if (type != null) {
final PyType pyType = PyTypeParser.getTypeByName(parameter, type);
if (pyType instanceof PyClassType) {
myBody.addItem(": ").
addWith(new LinkWrapper(PythonDocumentationProvider.LINK_TYPE_PARAM),
$(pyType.getName()));
}
else {
myBody.addItem(": ").addItem(type);
}
}
if (desc != null) {
myEpilog.addItem(BR).addItem(desc);
}
return type != null;
}
return false;
}
private static Pair<String, String> getTypeAndDescr(String docString, @NotNull PyNamedParameter followed) {
StructuredDocString structuredDocString = DocStringUtil.parse(docString);
String type = null;
String desc = null;
if (structuredDocString != null) {
final String name = followed.getName();
type = structuredDocString.getParamType(name);
desc = structuredDocString.getParamDescription(name);
}
return Pair.create(type, desc);
}
private void addAttributeDoc() {
PyClass cls = PsiTreeUtil.getParentOfType(myElement, PyClass.class);
assert cls != null;
String type = PyUtil.isInstanceAttribute((PyExpression)myElement) ? "Instance attribute " : "Class attribute ";
myProlog
.addItem(type).addWith(TagBold, $().addWith(TagCode, $(((PyTargetExpression)myElement).getName())))
.addItem(" of class ").addWith(PythonDocumentationProvider.LinkMyClass, $().addWith(TagCode, $(cls.getName()))).addItem(BR)
;
final String docString = ((PyTargetExpression)myElement).getDocStringValue();
if (docString != null) {
addFormattedDocString(myElement, docString, myBody, myEpilog);
}
}
public static String[] removeCommonIndentation(String docstring) {
// detect common indentation
String[] lines = LineTokenizer.tokenize(docstring, false);
boolean isFirst = true;
int cutWidth = Integer.MAX_VALUE;
int firstIndentedLine = 0;
for (String frag : lines) {
if (frag.length() == 0) continue;
int padWidth = 0;
final Matcher matcher = ourSpacesPattern.matcher(frag);
if (matcher.find()) {
padWidth = matcher.end();
}
if (isFirst) {
isFirst = false;
if (padWidth == 0) { // first line may have zero padding
firstIndentedLine = 1;
continue;
}
}
if (padWidth < cutWidth) cutWidth = padWidth;
}
// remove common indentation
if (cutWidth > 0 && cutWidth < Integer.MAX_VALUE) {
for (int i = firstIndentedLine; i < lines.length; i += 1) {
if (lines[i].length() >= cutWidth)
lines[i] = lines[i].substring(cutWidth);
}
}
List<String> result = new ArrayList<String>();
for (String line : lines) {
if (line.startsWith(PyConsoleUtil.ORDINARY_PROMPT)) break;
result.add(line);
}
return result.toArray(new String[result.size()]);
}
private void addModulePath(PyFile followed) {
// what to prepend to a module description?
final VirtualFile file = followed.getVirtualFile();
if (file == null) {
myProlog.addWith(TagSmall, $(PyBundle.message("QDOC.module.path.unknown")));
}
else {
final String path = file.getPath();
RootFinder finder = new RootFinder(path);
RootVisitorHost.visitRoots(followed, finder);
final String rootPath = finder.getResult();
if (rootPath != null) {
String afterPart = path.substring(rootPath.length());
myProlog.addWith(TagSmall, $(rootPath).addWith(TagBold, $(afterPart)));
}
else {
myProlog.addWith(TagSmall, $(path));
}
}
}
private static class RootFinder implements RootVisitor {
private String myResult;
private String myPath;
private RootFinder(String path) {
myPath = path;
}
public boolean visitRoot(VirtualFile root, Module module, Sdk sdk, boolean isModuleSource) {
String vpath = VfsUtilCore.urlToPath(root.getUrl());
if (myPath.startsWith(vpath)) {
myResult = vpath;
return false;
}
else {
return true;
}
}
String getResult() {
return myResult;
}
}
}