Use documentation from Python stubs for Quick Documentation view (PY-22685)
[idea/community.git] / python / src / com / jetbrains / python / documentation / PyDocumentationBuilder.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.documentation;
17
18 import com.intellij.lang.ASTNode;
19 import com.intellij.openapi.module.Module;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.projectRoots.Sdk;
22 import com.intellij.openapi.util.Pair;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.util.text.LineTokenizer;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.openapi.vfs.VfsUtilCore;
27 import com.intellij.openapi.vfs.VirtualFile;
28 import com.intellij.psi.PsiElement;
29 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
30 import com.intellij.psi.util.PsiTreeUtil;
31 import com.intellij.util.ArrayUtil;
32 import com.jetbrains.python.*;
33 import com.jetbrains.python.console.PyConsoleUtil;
34 import com.jetbrains.python.documentation.docstrings.DocStringUtil;
35 import com.jetbrains.python.documentation.docstrings.PyStructuredDocstringFormatter;
36 import com.jetbrains.python.psi.*;
37 import com.jetbrains.python.psi.impl.PyBuiltinCache;
38 import com.jetbrains.python.psi.impl.PyCallExpressionHelper;
39 import com.jetbrains.python.psi.impl.PyPsiUtils;
40 import com.jetbrains.python.psi.resolve.PyResolveContext;
41 import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
42 import com.jetbrains.python.psi.resolve.RootVisitor;
43 import com.jetbrains.python.psi.resolve.RootVisitorHost;
44 import com.jetbrains.python.psi.types.*;
45 import com.jetbrains.python.pyi.PyiUtil;
46 import com.jetbrains.python.toolbox.ChainIterable;
47 import com.jetbrains.python.toolbox.Maybe;
48 import org.jetbrains.annotations.NotNull;
49 import org.jetbrains.annotations.Nullable;
50
51 import java.io.FileNotFoundException;
52 import java.io.FileReader;
53 import java.io.IOException;
54 import java.util.ArrayList;
55 import java.util.List;
56 import java.util.regex.Matcher;
57 import java.util.regex.Pattern;
58
59 import static com.jetbrains.python.documentation.DocumentationBuilderKit.*;
60
61 public class PyDocumentationBuilder {
62   private final PsiElement myElement;
63   private final PsiElement myOriginalElement;
64   private ChainIterable<String> myResult;
65   private final ChainIterable<String> myProlog;      // sequence for reassignment info, etc
66   private final ChainIterable<String> myBody;        // sequence for doc string
67   private final ChainIterable<String> myEpilog;      // sequence for doc "copied from" notices and such
68
69   private static final Pattern ourSpacesPattern = Pattern.compile("^\\s+");
70   private final ChainIterable<String> myReassignmentChain;
71
72   public PyDocumentationBuilder(PsiElement element, PsiElement originalElement) {
73     myElement = element;
74     myOriginalElement = originalElement;
75     myResult = new ChainIterable<>();
76     myProlog = new ChainIterable<>();
77     myBody = new ChainIterable<>();
78     myEpilog = new ChainIterable<>();
79
80     myResult.add(myProlog).addWith(TagCode, myBody).add(myEpilog); // pre-assemble; then add stuff to individual cats as needed
81     myResult = wrapInTag("html", wrapInTag("body", myResult));
82     myReassignmentChain = new ChainIterable<>();
83   }
84
85   @Nullable
86   public String build() {
87     final TypeEvalContext context = TypeEvalContext.userInitiated(myElement.getProject(), myElement.getContainingFile());
88     final PsiElement outerElement = myOriginalElement != null ? myOriginalElement.getParent() : null;
89
90     final PsiElement elementDefinition = resolveToDocStringOwner();
91     final boolean isProperty = buildFromProperty(elementDefinition, outerElement, context);
92
93     if (myProlog.isEmpty() && !isProperty && !isAttribute()) {
94       myProlog.add(myReassignmentChain);
95     }
96
97     if (elementDefinition instanceof PyDocStringOwner) {
98       buildFromDocstring(elementDefinition, isProperty);
99     }
100     else if (isAttribute()) {
101       buildFromAttributeDoc();
102     }
103     else if (elementDefinition instanceof PyNamedParameter) {
104       buildFromParameter(context, outerElement, elementDefinition);
105     }
106     else if (elementDefinition != null && outerElement instanceof PyReferenceExpression) {
107       myBody.addItem(combUp("\nInferred type: "));
108       PythonDocumentationProvider.describeTypeWithLinks(context.getType((PyReferenceExpression)outerElement), context, outerElement, myBody);
109     }
110
111     if (elementDefinition != null) {
112       final ASTNode node = elementDefinition.getNode();
113       if (node != null && PythonDialectsTokenSetProvider.INSTANCE.getKeywordTokens().contains(node.getElementType())) {
114         String documentationName = elementDefinition.getText();
115         if (node.getElementType() == PyTokenTypes.AS_KEYWORD || node.getElementType() == PyTokenTypes.ELSE_KEYWORD) {
116           final PyTryExceptStatement statement = PsiTreeUtil.getParentOfType(elementDefinition, PyTryExceptStatement.class);
117           if (statement != null) documentationName = "try";
118         }
119         else if (node.getElementType() == PyTokenTypes.IN_KEYWORD) {
120           final PyForStatement statement = PsiTreeUtil.getParentOfType(elementDefinition, PyForStatement.class);
121           if (statement != null) documentationName = "for";
122         }
123         buildForKeyword(documentationName);
124       }
125     }
126     final String url = PythonDocumentationProvider.getUrlFor(myElement, myOriginalElement, false);
127     if (url != null) {
128       myEpilog.addItem(BR);
129       myEpilog.addWith(TagBold, $("External documentation:"));
130       myEpilog.addItem(BR);
131       myEpilog.addItem("<a href=\"").addItem(url).addItem("\">").addItem(url).addItem("</a>");
132     }
133
134     if (myBody.isEmpty() && myEpilog.isEmpty()) {
135       return null; // got nothing substantial to say!
136     }
137     else {
138       return myResult.toString();
139     }
140   }
141
142   private void buildForKeyword(@NotNull final String name) {
143     try {
144       final FileReader reader = new FileReader(PythonHelpersLocator.getHelperPath("/tools/python_keywords/" + name));
145       try {
146         final String text = FileUtil.loadTextAndClose(reader);
147         myEpilog.addItem(StringUtil.convertLineSeparators(text, "\n"));
148       }
149       catch (IOException ignored) {
150       }
151       finally {
152         try {
153           reader.close();
154         }
155         catch (IOException ignored) {
156         }
157       }
158     }
159     catch (FileNotFoundException ignored) {
160     }
161   }
162
163   private void buildFromParameter(@NotNull final TypeEvalContext context, @Nullable final PsiElement outerElement,
164                                   @NotNull final PsiElement elementDefinition) {
165     myBody.addItem(combUp("Parameter " + PyUtil.getReadableRepr(elementDefinition, false)));
166     final boolean typeFromDocstringAdded = addTypeAndDescriptionFromDocstring((PyNamedParameter)elementDefinition);
167     if (outerElement instanceof PyExpression) {
168       final PyType type = context.getType((PyExpression)outerElement);
169       if (type != null) {
170         String typeString = null;
171         if (type instanceof PyDynamicallyEvaluatedType) {
172           if (!typeFromDocstringAdded) {
173             typeString = "\nDynamically inferred type: ";
174           }
175         }
176         else {
177           if (outerElement.getReference() != null) {
178             final PsiElement target = outerElement.getReference().resolve();
179
180             if (target instanceof PyTargetExpression) {
181               final String targetName = ((PyTargetExpression)target).getName();
182               if (targetName != null && targetName.equals(((PyNamedParameter)elementDefinition).getName())) {
183                 typeString = "\nReassigned value has type: ";
184               }
185             }
186           }
187         }
188         if (typeString == null && !typeFromDocstringAdded) {
189           typeString = "\nInferred type: ";
190         }
191         if (typeString != null) {
192           myBody.addItem(combUp(typeString));
193           PythonDocumentationProvider.describeTypeWithLinks(type, context, elementDefinition, myBody);
194         }
195       }
196     }
197   }
198
199   private boolean buildFromProperty(PsiElement elementDefinition, @Nullable final PsiElement outerElement,
200                                     @NotNull final TypeEvalContext context) {
201     if (myOriginalElement == null) {
202       return false;
203     }
204     final String elementName = myOriginalElement.getText();
205     if (!PyNames.isIdentifier(elementName)) {
206       return false;
207     }
208     if (!(outerElement instanceof PyQualifiedExpression)) {
209       return false;
210     }
211     final PyExpression qualifier = ((PyQualifiedExpression)outerElement).getQualifier();
212     if (qualifier == null) {
213       return false;
214     }
215     final PyType type = context.getType(qualifier);
216     if (!(type instanceof PyClassType)) {
217       return false;
218     }
219     final PyClass cls = ((PyClassType)type).getPyClass();
220     final Property property = cls.findProperty(elementName, true, null);
221     if (property == null) {
222       return false;
223     }
224
225     final AccessDirection direction = AccessDirection.of((PyElement)outerElement);
226     final Maybe<PyCallable> accessor = property.getByDirection(direction);
227     myProlog.addItem("property ").addWith(TagBold, $().addWith(TagCode, $(elementName)))
228       .addItem(" of ").add(PythonDocumentationProvider.describeClass(cls, TagCode, true, true));
229     if (accessor.isDefined() && property.getDoc() != null) {
230       myBody.addItem(": ").addItem(property.getDoc()).addItem(BR);
231     }
232     else {
233       final PyCallable getter = property.getGetter().valueOrNull();
234       if (getter != null && getter != myElement && getter instanceof PyFunction) {
235         // not in getter, getter's doc comment may be useful
236         final PyStringLiteralExpression docstring = getEffectiveDocStringExpression((PyFunction)getter);
237         if (docstring != null) {
238           myProlog
239             .addItem(BR).addWith(TagItalic, $("Copied from getter:")).addItem(BR)
240             .addItem(docstring.getStringValue())
241           ;
242         }
243       }
244       myBody.addItem(BR);
245     }
246     myBody.addItem(BR);
247     if (accessor.isDefined() && accessor.value() == null) elementDefinition = null;
248     final String accessorKind = getAccessorKind(direction);
249     if (elementDefinition != null) {
250       myEpilog.addWith(TagSmall, $(BR, BR, accessorKind, " of property")).addItem(BR);
251     }
252
253     if (!(elementDefinition instanceof PyDocStringOwner)) {
254       myBody.addWith(TagItalic, elementDefinition != null ? $("Declaration: ") : $(accessorKind + " is not defined.")).addItem(BR);
255       if (elementDefinition != null) {
256         myBody.addItem(combUp(PyUtil.getReadableRepr(elementDefinition, false)));
257       }
258     }
259     return true;
260   }
261
262   @NotNull
263   private static String getAccessorKind(@NotNull final AccessDirection dir) {
264     final String accessorKind;
265     if (dir == AccessDirection.READ) {
266       accessorKind = "Getter";
267     }
268     else if (dir == AccessDirection.WRITE) {
269       accessorKind = "Setter";
270     }
271     else {
272       accessorKind = "Deleter";
273     }
274     return accessorKind;
275   }
276
277   private void buildFromDocstring(@NotNull final PsiElement elementDefinition, boolean isProperty) {
278     PyClass pyClass = null;
279     final PyStringLiteralExpression docStringExpression = getEffectiveDocStringExpression((PyDocStringOwner)elementDefinition);
280
281     if (elementDefinition instanceof PyClass) {
282       pyClass = (PyClass)elementDefinition;
283       myBody.add(PythonDocumentationProvider.describeDecorators(pyClass, TagItalic, BR, LCombUp));
284       myBody.add(PythonDocumentationProvider.describeClass(pyClass, TagBold, true, false));
285     }
286     else if (elementDefinition instanceof PyFunction) {
287       final PyFunction pyFunction = (PyFunction)elementDefinition;
288       if (!isProperty) {
289         pyClass = pyFunction.getContainingClass();
290         if (pyClass != null) {
291           myBody.addWith(TagSmall, PythonDocumentationProvider.describeClass(pyClass, TagCode, true, true)).addItem(BR).addItem(BR);
292         }
293       }
294       myBody.add(PythonDocumentationProvider.describeDecorators(pyFunction, TagItalic, BR, LCombUp))
295             .add(PythonDocumentationProvider.describeFunction(pyFunction, TagBold, LCombUp));
296       if (docStringExpression == null) {
297         addInheritedDocString(pyFunction, pyClass);
298       }
299     }
300     else if (elementDefinition instanceof PyFile) {
301       addModulePath((PyFile)elementDefinition);
302     }
303     if (docStringExpression != null) {
304       myBody.addItem(BR);
305       addFormattedDocString(myElement, docStringExpression.getStringValue(), myBody, myEpilog);
306     }
307   }
308
309   private boolean isAttribute() {
310     return myElement instanceof PyTargetExpression && PyUtil.isAttribute((PyTargetExpression)myElement);
311   }
312
313   @Nullable
314   private PsiElement resolveToDocStringOwner() {
315     // here the ^Q target is already resolved; the resolved element may point to intermediate assignments
316     if (myElement instanceof PyTargetExpression) {
317       final String targetName = myElement.getText();
318       myReassignmentChain.addWith(TagSmall, $(PyBundle.message("QDOC.assigned.to.$0", targetName)).addItem(BR));
319       final PyExpression assignedValue = ((PyTargetExpression)myElement).findAssignedValue();
320       if (assignedValue instanceof PyReferenceExpression) {
321         final PsiElement resolved = resolveWithoutImplicits((PyReferenceExpression)assignedValue);
322         if (resolved != null) {
323           return resolved;
324         }
325       }
326       return assignedValue;
327     }
328     if (myElement instanceof PyReferenceExpression) {
329       myReassignmentChain.addWith(TagSmall, $(PyBundle.message("QDOC.assigned.to.$0", myElement.getText())).addItem(BR));
330       return resolveWithoutImplicits((PyReferenceExpression)myElement);
331     }
332     // it may be a call to a standard wrapper
333     if (myElement instanceof PyCallExpression) {
334       final PyCallExpression call = (PyCallExpression)myElement;
335       final Pair<String, PyFunction> wrapInfo = PyCallExpressionHelper.interpretAsModifierWrappingCall(call, myOriginalElement);
336       if (wrapInfo != null) {
337         final String wrapperName = wrapInfo.getFirst();
338         final PyFunction wrappedFunction = wrapInfo.getSecond();
339         myReassignmentChain.addWith(TagSmall, $(PyBundle.message("QDOC.wrapped.in.$0", wrapperName)).addItem(BR));
340         return wrappedFunction;
341       }
342     }
343     return myElement;
344   }
345
346   private static PsiElement resolveWithoutImplicits(@NotNull PyReferenceExpression element) {
347     final QualifiedResolveResult resolveResult = element.followAssignmentsChain(PyResolveContext.noImplicits());
348     return resolveResult.isImplicit() ? null : resolveResult.getElement();
349   }
350
351   private void addInheritedDocString(@NotNull final PyFunction pyFunction, @Nullable final PyClass pyClass) {
352     boolean notFound = true;
353     final String methodName = pyFunction.getName();
354     if (pyClass == null || methodName == null) {
355       return;
356     }
357     final boolean isConstructor = PyNames.INIT.equals(methodName);
358     Iterable<PyClass> classes = pyClass.getAncestorClasses(null);
359     if (isConstructor) {
360       // look at our own class again and maybe inherit class's doc
361       classes = new ChainIterable<>(pyClass).add(classes);
362     }
363     for (PyClass ancestor : classes) {
364       PyStringLiteralExpression docstringElement = null;
365       PyFunction inherited = null;
366       boolean isFromClass = false;
367       if (isConstructor) docstringElement = getEffectiveDocStringExpression(pyClass);
368       if (docstringElement != null) {
369         isFromClass = true;
370       }
371       else {
372         inherited = ancestor.findMethodByName(methodName, false, null);
373       }
374       if (inherited != null) {
375         docstringElement = getEffectiveDocStringExpression(inherited);
376       }
377       if (docstringElement != null) {
378         final String inheritedDoc = docstringElement.getStringValue();
379         if (inheritedDoc.length() > 1) {
380           myEpilog.addItem(BR).addItem(BR);
381           final String ancestorName = ancestor.getName();
382           final String marker = (pyClass == ancestor) ? PythonDocumentationProvider.LINK_TYPE_CLASS : PythonDocumentationProvider.LINK_TYPE_PARENT;
383           final String ancestorLink =
384             $().addWith(new LinkWrapper(marker + ancestorName), $(ancestorName)).toString();
385           if (isFromClass) {
386             myEpilog.addItem(PyBundle.message("QDOC.copied.from.class.$0", ancestorLink));
387           }
388           else {
389             myEpilog.addItem(PyBundle.message("QDOC.copied.from.$0.$1", ancestorLink, methodName));
390           }
391           myEpilog.addItem(BR).addItem(BR);
392           final ChainIterable<String> formatted = new ChainIterable<>();
393           final ChainIterable<String> unformatted = new ChainIterable<>();
394           addFormattedDocString(pyFunction, inheritedDoc, formatted, unformatted);
395           myEpilog.addWith(TagCode, formatted).add(unformatted);
396           notFound = false;
397           break;
398         }
399       }
400     }
401
402     if (notFound) {
403       // above could have not worked because inheritance is not searched down to 'object'.
404       // for well-known methods, copy built-in doc string.
405       // TODO: also handle predefined __xxx__ that are not part of 'object'.
406       if (PyNames.UnderscoredAttributes.contains(methodName)) {
407         addPredefinedMethodDoc(pyFunction, methodName);
408       }
409     }
410   }
411
412   private void addPredefinedMethodDoc(PyFunction fun, String mothodName) {
413     final PyClassType objectType = PyBuiltinCache.getInstance(fun).getObjectType(); // old- and new-style classes share the __xxx__ stuff
414     if (objectType != null) {
415       final PyClass objectClass = objectType.getPyClass();
416       final PyFunction predefinedMethod = objectClass.findMethodByName(mothodName, false, null);
417       if (predefinedMethod != null) {
418         final PyStringLiteralExpression predefinedDocstring = getEffectiveDocStringExpression(predefinedMethod);
419         final String predefinedDoc = predefinedDocstring != null ? predefinedDocstring.getStringValue() : null;
420         if (predefinedDoc != null && predefinedDoc.length() > 1) { // only a real-looking doc string counts
421           addFormattedDocString(fun, predefinedDoc, myBody, myBody);
422           myEpilog.addItem(BR).addItem(BR).addItem(PyBundle.message("QDOC.copied.from.builtin"));
423         }
424       }
425     }
426   }
427
428   private static void addFormattedDocString(@NotNull PsiElement element,
429                                             @NotNull String docstring,
430                                             @NotNull ChainIterable<String> formattedOutput,
431                                             @NotNull ChainIterable<String> unformattedOutput) {
432     final Project project = element.getProject();
433
434     final List<String> formatted = PyStructuredDocstringFormatter.formatDocstring(element, docstring);
435     if (formatted != null) {
436       unformattedOutput.add(formatted);
437       return;
438     }
439
440     boolean isFirstLine;
441     final List<String> result = new ArrayList<>();
442     final String[] lines = removeCommonIndentation(docstring);
443
444     // reconstruct back, dropping first empty fragment as needed
445     isFirstLine = true;
446     final int tabSize = CodeStyleSettingsManager.getSettings(project).getTabSize(PythonFileType.INSTANCE);
447     for (String line : lines) {
448       if (isFirstLine && ourSpacesPattern.matcher(line).matches()) continue; // ignore all initial whitespace
449       if (isFirstLine) {
450         isFirstLine = false;
451       }
452       else {
453         result.add(BR);
454       }
455       int leadingTabs = 0;
456       while (leadingTabs < line.length() && line.charAt(leadingTabs) == '\t') {
457         leadingTabs++;
458       }
459       if (leadingTabs > 0) {
460         line = StringUtil.repeatSymbol(' ', tabSize * leadingTabs) + line.substring(leadingTabs);
461       }
462       result.add(combUp(line));
463     }
464     formattedOutput.add(result);
465   }
466
467   /**
468    * Adds type and description representation from function docstring
469    *
470    * @param parameter parameter of a function
471    * @return true if type from docstring was added
472    */
473   private boolean addTypeAndDescriptionFromDocstring(@NotNull final PyNamedParameter parameter) {
474     final PyFunction function = PsiTreeUtil.getParentOfType(parameter, PyFunction.class);
475     if (function != null) {
476       final String docString = PyPsiUtils.strValue(getEffectiveDocStringExpression(function));
477       final Pair<String, String> typeAndDescr = getTypeAndDescription(docString, parameter);
478
479       final String type = typeAndDescr.first;
480       final String description = typeAndDescr.second;
481
482       if (type != null) {
483         final PyType pyType = PyTypeParser.getTypeByName(parameter, type);
484         if (pyType instanceof PyClassType) {
485           myBody.addItem(": ").addWith(new LinkWrapper(PythonDocumentationProvider.LINK_TYPE_PARAM), $(pyType.getName()));
486         }
487         else {
488           myBody.addItem(": ").addItem(type);
489         }
490       }
491
492       if (description != null) {
493         myEpilog.addItem(BR).addItem(description);
494       }
495
496       return type != null;
497     }
498
499     return false;
500   }
501
502   private static Pair<String, String> getTypeAndDescription(@Nullable final String docString, @NotNull final PyNamedParameter followed) {
503     String type = null;
504     String desc = null;
505     if (docString != null) {
506       final StructuredDocString structuredDocString = DocStringUtil.parse(docString);
507       final String name = followed.getName();
508       type = structuredDocString.getParamType(name);
509       desc = structuredDocString.getParamDescription(name);
510     }
511     return Pair.create(type, desc);
512   }
513
514   private void buildFromAttributeDoc() {
515     final PyClass cls = PsiTreeUtil.getParentOfType(myElement, PyClass.class);
516     assert cls != null;
517     final String type = PyUtil.isInstanceAttribute((PyExpression)myElement) ? "Instance attribute " : "Class attribute ";
518     myProlog
519       .addItem(type).addWith(TagBold, $().addWith(TagCode, $(((PyTargetExpression)myElement).getName())))
520       .addItem(" of class ").addWith(PythonDocumentationProvider.LinkMyClass, $().addWith(TagCode, $(cls.getName()))).addItem(BR);
521
522     final String docString = PyPsiUtils.strValue(getEffectiveDocStringExpression((PyTargetExpression)myElement));
523     if (docString != null) {
524       addFormattedDocString(myElement, docString, myBody, myEpilog);
525     }
526   }
527
528   public static String[] removeCommonIndentation(@NotNull final String docstring) {
529     // detect common indentation
530     final String[] lines = LineTokenizer.tokenize(docstring, false);
531     boolean isFirst = true;
532     int cutWidth = Integer.MAX_VALUE;
533     int firstIndentedLine = 0;
534     for (String frag : lines) {
535       if (frag.length() == 0) continue;
536       int padWidth = 0;
537       final Matcher matcher = ourSpacesPattern.matcher(frag);
538       if (matcher.find()) {
539         padWidth = matcher.end();
540       }
541       if (isFirst) {
542         isFirst = false;
543         if (padWidth == 0) {    // first line may have zero padding
544           firstIndentedLine = 1;
545           continue;
546         }
547       }
548       if (padWidth < cutWidth) cutWidth = padWidth;
549     }
550     // remove common indentation
551     if (cutWidth > 0 && cutWidth < Integer.MAX_VALUE) {
552       for (int i = firstIndentedLine; i < lines.length; i += 1) {
553         if (lines[i].length() >= cutWidth) {
554           lines[i] = lines[i].substring(cutWidth);
555         }
556       }
557     }
558     final List<String> result = new ArrayList<>();
559     for (String line : lines) {
560       if (line.startsWith(PyConsoleUtil.ORDINARY_PROMPT)) break;
561       result.add(line);
562     }
563     return ArrayUtil.toStringArray(result);
564   }
565
566   private void addModulePath(@NotNull PyFile followed) {
567     // what to prepend to a module description?
568     final VirtualFile file = followed.getVirtualFile();
569     if (file == null) {
570       myProlog.addWith(TagSmall, $(PyBundle.message("QDOC.module.path.unknown")));
571     }
572     else {
573       final String path = file.getPath();
574       final RootFinder finder = new RootFinder(path);
575       RootVisitorHost.visitRoots(followed, finder);
576       final String rootPath = finder.getResult();
577       if (rootPath != null) {
578         final String afterPart = path.substring(rootPath.length());
579         myProlog.addWith(TagSmall, $(rootPath).addWith(TagBold, $(afterPart)));
580       }
581       else {
582         myProlog.addWith(TagSmall, $(path));
583       }
584     }
585   }
586
587   @Nullable
588   static PyStringLiteralExpression getEffectiveDocStringExpression(@NotNull PyDocStringOwner owner) {
589     final PyStringLiteralExpression expression = owner.getDocStringExpression();
590     if (expression != null && StringUtil.isNotEmpty(PyPsiUtils.strValue(expression))) {
591       return expression;
592     }
593     final PsiElement original = PyiUtil.getOriginalElement(owner);
594     final PyDocStringOwner originalOwner = PyUtil.as(original, PyDocStringOwner.class);
595     return originalOwner != null ? originalOwner.getDocStringExpression() : null;
596   }
597
598   private static class RootFinder implements RootVisitor {
599     private String myResult;
600     private final String myPath;
601
602     private RootFinder(String path) {
603       myPath = path;
604     }
605
606     public boolean visitRoot(@NotNull VirtualFile root, Module module, Sdk sdk, boolean isModuleSource) {
607       final String vpath = VfsUtilCore.urlToPath(root.getUrl());
608       if (myPath.startsWith(vpath)) {
609         myResult = vpath;
610         return false;
611       }
612       else {
613         return true;
614       }
615     }
616
617     String getResult() {
618       return myResult;
619     }
620   }
621 }