replaced <code></code> with more concise {@code}
[idea/community.git] / python / src / com / jetbrains / python / documentation / PythonDocumentationProvider.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.ide.actions.ShowSettingsUtilImpl;
19 import com.intellij.lang.documentation.AbstractDocumentationProvider;
20 import com.intellij.lang.documentation.ExternalDocumentationProvider;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.application.ModalityState;
23 import com.intellij.openapi.editor.Editor;
24 import com.intellij.openapi.extensions.Extensions;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.projectRoots.Sdk;
27 import com.intellij.openapi.roots.ProjectRootManager;
28 import com.intellij.openapi.ui.Messages;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.psi.*;
31 import com.intellij.psi.util.PsiTreeUtil;
32 import com.intellij.psi.util.QualifiedName;
33 import com.jetbrains.python.PyNames;
34 import com.jetbrains.python.PythonDialectsTokenSetProvider;
35 import com.jetbrains.python.console.PydevConsoleRunner;
36 import com.jetbrains.python.console.PydevDocumentationProvider;
37 import com.jetbrains.python.documentation.docstrings.DocStringUtil;
38 import com.jetbrains.python.psi.*;
39 import com.jetbrains.python.psi.impl.PyBuiltinCache;
40 import com.jetbrains.python.psi.resolve.QualifiedNameFinder;
41 import com.jetbrains.python.psi.types.PyClassType;
42 import com.jetbrains.python.psi.types.PyType;
43 import com.jetbrains.python.psi.types.PyTypeParser;
44 import com.jetbrains.python.psi.types.TypeEvalContext;
45 import com.jetbrains.python.toolbox.ChainIterable;
46 import com.jetbrains.python.toolbox.FP;
47 import org.apache.commons.httpclient.HttpClient;
48 import org.apache.commons.httpclient.methods.HeadMethod;
49 import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
50 import org.jetbrains.annotations.NonNls;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53
54 import java.io.File;
55 import java.io.IOException;
56 import java.util.Collections;
57 import java.util.List;
58
59 import static com.jetbrains.python.documentation.DocumentationBuilderKit.*;
60
61 /**
62  * Provides quick docs for classes, methods, and functions.
63  * Generates documentation stub
64  */
65 public class PythonDocumentationProvider extends AbstractDocumentationProvider implements ExternalDocumentationProvider {
66
67   @NonNls static final String LINK_TYPE_CLASS = "#class#";
68   @NonNls static final String LINK_TYPE_PARENT = "#parent#";
69   @NonNls static final String LINK_TYPE_PARAM = "#param#";
70   @NonNls static final String LINK_TYPE_TYPENAME = "#typename#";
71
72   // provides ctrl+hover info
73   @Override
74   @Nullable
75   public String getQuickNavigateInfo(PsiElement element, @NotNull PsiElement originalElement) {
76     for (PythonDocumentationQuickInfoProvider point : PythonDocumentationQuickInfoProvider.EP_NAME.getExtensions()) {
77       final String info = point.getQuickInfo(originalElement);
78       if (info != null) {
79         return info;
80       }
81     }
82
83     if (element instanceof PyFunction) {
84       final PyFunction func = (PyFunction)element;
85       final StringBuilder cat = new StringBuilder();
86       final PyClass cls = func.getContainingClass();
87       if (cls != null) {
88         final String clsName = cls.getName();
89         cat.append("class ").append(clsName).append("\n");
90         // It would be nice to have class import info here, but we don't know the ctrl+hovered reference and context
91       }
92       String summary = "";
93       final PyStringLiteralExpression docStringExpression = PyDocumentationBuilder.getEffectiveDocStringExpression(func);
94       if (docStringExpression != null) {
95         final StructuredDocString docString = DocStringUtil.parse(docStringExpression.getStringValue(), docStringExpression);
96         summary = docString.getSummary();
97       }
98       return $(cat.toString()).add(describeDecorators(func, LSame2, ", ", LSame1)).add(describeFunction(func, LSame2, LSame1))
99                               .toString() + "\n" + summary;
100     }
101     else if (element instanceof PyClass) {
102       final PyClass cls = (PyClass)element;
103       String summary = "";
104       PyStringLiteralExpression docStringExpression = PyDocumentationBuilder.getEffectiveDocStringExpression(cls);
105       if (docStringExpression == null) {
106         final PyFunction initOrNew = cls.findInitOrNew(false, null);
107         if (initOrNew != null) {
108           docStringExpression = PyDocumentationBuilder.getEffectiveDocStringExpression(initOrNew);
109         }
110       }
111       if (docStringExpression != null) {
112         final StructuredDocString docString = DocStringUtil.parse(docStringExpression.getStringValue(), docStringExpression);
113         summary = docString.getSummary();
114       }
115
116       return describeDecorators(cls, LSame2, ", ", LSame1).add(describeClass(cls, LSame2, false, false)).toString() + "\n" + summary;
117     }
118     else if (element instanceof PyExpression) {
119       return describeExpression((PyExpression)element, originalElement);
120     }
121     return null;
122   }
123
124   /**
125    * Creates a HTML description of function definition.
126    *
127    * @param fun             the function
128    * @param funcNameWrapper puts a tag around the function name
129    * @param escaper         sanitizes values that come directly from doc string or code
130    * @return chain of strings for further chaining
131    */
132   @NotNull
133   static ChainIterable<String> describeFunction(@NotNull PyFunction fun,
134                                                 FP.Lambda1<Iterable<String>, Iterable<String>> funcNameWrapper,
135                                                 @NotNull FP.Lambda1<String, String> escaper
136   ) {
137     final ChainIterable<String> cat = new ChainIterable<>();
138     final String name = fun.getName();
139     cat.addItem("def ").addWith(funcNameWrapper, $(name));
140     cat.addItem(escaper.apply(PyUtil.getReadableRepr(fun.getParameterList(), false)));
141     if (!PyNames.INIT.equals(name)) {
142       cat.addItem(escaper.apply("\nInferred type: "));
143       describeTypeWithLinks(fun, cat);
144       cat.addItem(BR);
145     }
146     return cat;
147   }
148
149   @Nullable
150   private static String describeExpression(@NotNull PyExpression expr, @NotNull PsiElement originalElement) {
151     final String name = expr.getName();
152     if (name != null) {
153       final StringBuilder result = new StringBuilder((expr instanceof PyNamedParameter) ? "parameter" : "variable");
154       result.append(String.format(" \"%s\"", name));
155       if (expr instanceof PyNamedParameter) {
156         final PyFunction function = PsiTreeUtil.getParentOfType(expr, PyFunction.class);
157         if (function != null) {
158           result.append(" of ").append(function.getContainingClass() == null ? "function" : "method");
159           result.append(String.format(" \"%s\"", function.getName()));
160         }
161       }
162       if (originalElement instanceof PyTypedElement) {
163         final String typeName = getTypeName(((PyTypedElement)originalElement));
164         result
165           .append("\n")
166           .append(String.format("Inferred type: %s", typeName));
167       }
168       return result.toString();
169     }
170     return null;
171   }
172
173   @NotNull
174   private static String getTypeName(@NotNull PyTypedElement element) {
175     final TypeEvalContext context = TypeEvalContext.userInitiated(element.getProject(), element.getContainingFile());
176     return getTypeName(context.getType(element), context);
177   }
178
179   /**
180    * @param type    type which name will be calculated
181    * @param context type evaluation context
182    * @return string representation of the type
183    */
184   @NotNull
185   public static String getTypeName(@Nullable PyType type, @NotNull TypeEvalContext context) {
186     return buildTypeModel(type, context).asString();
187   }
188
189   private static void describeTypeWithLinks(@NotNull PyTypedElement element, @NotNull ChainIterable<String> body) {
190     final TypeEvalContext context = TypeEvalContext.userInitiated(element.getProject(), element.getContainingFile());
191     describeTypeWithLinks(context.getType(element), context, element, body);
192   }
193
194   /**
195    * @param type    type which description will be calculated.
196    *                Description is the same as {@link PythonDocumentationProvider#getTypeDescription(PyType, TypeEvalContext)} gives but
197    *                types are converted to links.
198    * @param context type evaluation context
199    * @param anchor  anchor element
200    * @param body    body to be used to append description
201    */
202   public static void describeTypeWithLinks(@Nullable PyType type,
203                                            @NotNull TypeEvalContext context,
204                                            @NotNull PsiElement anchor,
205                                            @NotNull ChainIterable<String> body) {
206     buildTypeModel(type, context).toBodyWithLinks(body, anchor);
207   }
208
209   /**
210    * @param type    type which description will be calculated
211    * @param context type evaluation context
212    * @return more user-friendly description than result of {@link PythonDocumentationProvider#getTypeName(PyType, TypeEvalContext)}.
213    * {@code Any} is excluded from {@code Union[Any, ...]}-like types.
214    */
215   @NotNull
216   public static String getTypeDescription(@Nullable PyType type, @NotNull TypeEvalContext context) {
217     return buildTypeModel(type, context).asDescription();
218   }
219
220   @NotNull
221   private static PyTypeModelBuilder.TypeModel buildTypeModel(@Nullable PyType type, @NotNull TypeEvalContext context) {
222     return new PyTypeModelBuilder(context).build(type, true);
223   }
224
225   @NotNull
226   static ChainIterable<String> describeDecorators(@NotNull PyDecoratable what,
227                                                   FP.Lambda1<Iterable<String>, Iterable<String>> decoNameWrapper,
228                                                   @NotNull String decoSeparator,
229                                                   FP.Lambda1<String, String> escaper) {
230     final ChainIterable<String> cat = new ChainIterable<>();
231     final PyDecoratorList decoList = what.getDecoratorList();
232     if (decoList != null) {
233       for (PyDecorator deco : decoList.getDecorators()) {
234         cat.add(describeDeco(deco, decoNameWrapper, escaper)).addItem(decoSeparator); // can't easily pass describeDeco to map() %)
235       }
236     }
237     return cat;
238   }
239
240   /**
241    * Creates a HTML description of function definition.
242    *
243    * @param cls         the class
244    * @param nameWrapper wrapper to render the name with
245    * @param allowHtml
246    * @param linkOwnName if true, add link to class's own name  @return cat for easy chaining
247    */
248   @NotNull
249   static ChainIterable<String> describeClass(@NotNull PyClass cls,
250                                              FP.Lambda1<Iterable<String>, Iterable<String>> nameWrapper,
251                                              boolean allowHtml,
252                                              boolean linkOwnName) {
253     final ChainIterable<String> cat = new ChainIterable<>();
254     final String name = cls.getName();
255     cat.addItem("class ");
256     if (allowHtml && linkOwnName) {
257       cat.addWith(LinkMyClass, $(name));
258     }
259     else {
260       cat.addWith(nameWrapper, $(name));
261     }
262     final PyExpression[] ancestors = cls.getSuperClassExpressions();
263     if (ancestors.length > 0) {
264       cat.addItem("(");
265       boolean isNotFirst = false;
266       for (PyExpression parent : ancestors) {
267         final String parentName = parent.getName();
268         if (parentName == null) {
269           continue;
270         }
271         if (isNotFirst) {
272           cat.addItem(", ");
273         }
274         else {
275           isNotFirst = true;
276         }
277         if (allowHtml) {
278           cat.addWith(new LinkWrapper(LINK_TYPE_PARENT + parentName), $(parentName));
279         }
280         else {
281           cat.addItem(parentName);
282         }
283       }
284       cat.addItem(")");
285     }
286     return cat;
287   }
288
289   //
290   @NotNull
291   private static Iterable<String> describeDeco(@NotNull PyDecorator deco,
292                                                FP.Lambda1<Iterable<String>, Iterable<String>> nameWrapper,
293                                                //  addWith in tags, if need be
294                                                FP.Lambda1<String, String> argWrapper
295                                                // add escaping, if need be
296   ) {
297     final ChainIterable<String> cat = new ChainIterable<>();
298     cat.addItem("@").addWith(nameWrapper, $(PyUtil.getReadableRepr(deco.getCallee(), true)));
299     if (deco.hasArgumentList()) {
300       final PyArgumentList arglist = deco.getArgumentList();
301       if (arglist != null) {
302         cat
303           .addItem("(")
304           .add(interleave(FP.map(FP.combine(LReadableRepr, argWrapper), arglist.getArguments()), ", "))
305           .addItem(")")
306         ;
307       }
308     }
309     return cat;
310   }
311
312   // provides ctrl+Q doc
313   @Override
314   public String generateDoc(@Nullable PsiElement element, @Nullable PsiElement originalElement) {
315     if (element != null && PydevConsoleRunner.isInPydevConsole(element) ||
316         originalElement != null && PydevConsoleRunner.isInPydevConsole(originalElement)) {
317       return PydevDocumentationProvider.createDoc(element, originalElement);
318     }
319     return new PyDocumentationBuilder(element, originalElement).build();
320   }
321
322   @Override
323   public PsiElement getDocumentationElementForLink(PsiManager psiManager, @NotNull String link, @NotNull PsiElement context) {
324     if (link.equals(LINK_TYPE_CLASS)) {
325       return inferContainingClassOf(context);
326     }
327     else if (link.equals(LINK_TYPE_PARAM)) {
328       return inferClassOfParameter(context);
329     }
330     else if (link.startsWith(LINK_TYPE_PARENT)) {
331       final PyClass cls = inferContainingClassOf(context);
332       if (cls != null) {
333         final String desiredName = link.substring(LINK_TYPE_PARENT.length());
334         for (PyClass parent : cls.getAncestorClasses(null)) {
335           final String parentName = parent.getName();
336           if (parentName != null && parentName.equals(desiredName)) return parent;
337         }
338       }
339     }
340     else if (link.startsWith(LINK_TYPE_TYPENAME)) {
341       final String typeName = link.substring(LINK_TYPE_TYPENAME.length());
342       final PyType type = PyTypeParser.getTypeByName(context, typeName);
343       if (type instanceof PyClassType) {
344         return ((PyClassType)type).getPyClass();
345       }
346     }
347     return null;
348   }
349
350   @Override
351   public List<String> getUrlFor(PsiElement element, PsiElement originalElement) {
352     final String url = getUrlFor(element, originalElement, true);
353     return url == null ? null : Collections.singletonList(url);
354   }
355
356   @Nullable
357   public static String getUrlFor(PsiElement element, PsiElement originalElement, boolean checkExistence) {
358     PsiFileSystemItem file = element instanceof PsiFileSystemItem ? (PsiFileSystemItem)element : element.getContainingFile();
359     if (file == null) return null;
360     if (PyNames.INIT_DOT_PY.equals(file.getName())) {
361       file = file.getParent();
362       assert file != null;
363     }
364     final Sdk sdk = PyBuiltinCache.findSdkForFile(file);
365     if (sdk == null) {
366       return null;
367     }
368     final QualifiedName qName = QualifiedNameFinder.findCanonicalImportPath(element, originalElement);
369     if (qName == null) {
370       return null;
371     }
372     final PythonDocumentationMap map = PythonDocumentationMap.getInstance();
373     final String pyVersion = pyVersion(sdk.getVersionString());
374     PsiNamedElement namedElement = (element instanceof PsiNamedElement && !(element instanceof PsiFileSystemItem))
375                                    ? (PsiNamedElement)element
376                                    : null;
377     if (namedElement instanceof PyFunction && PyNames.INIT.equals(namedElement.getName())) {
378       final PyClass containingClass = ((PyFunction)namedElement).getContainingClass();
379       if (containingClass != null) {
380         namedElement = containingClass;
381       }
382     }
383     final String url = map.urlFor(qName, namedElement, pyVersion);
384     if (url != null) {
385       if (checkExistence && !pageExists(url)) {
386         return map.rootUrlFor(qName);
387       }
388       return url;
389     }
390     for (PythonDocumentationLinkProvider provider : Extensions.getExtensions(PythonDocumentationLinkProvider.EP_NAME)) {
391       final String providerUrl = provider.getExternalDocumentationUrl(element, originalElement);
392       if (providerUrl != null) {
393         if (checkExistence && !pageExists(providerUrl)) {
394           return provider.getExternalDocumentationRoot(sdk);
395         }
396         return providerUrl;
397       }
398     }
399     return null;
400   }
401
402   private static boolean pageExists(@NotNull String url) {
403     if (new File(url).exists()) {
404       return true;
405     }
406     final HttpClient client = new HttpClient();
407     final HttpConnectionManagerParams params = client.getHttpConnectionManager().getParams();
408     params.setSoTimeout(5 * 1000);
409     params.setConnectionTimeout(5 * 1000);
410
411     try {
412       final HeadMethod method = new HeadMethod(url);
413       final int rc = client.executeMethod(method);
414       if (rc == 404) {
415         return false;
416       }
417     }
418     catch (IllegalArgumentException e) {
419       return false;
420     }
421     catch (IOException ignored) {
422     }
423     return true;
424   }
425
426   @Nullable
427   public static String pyVersion(@Nullable String versionString) {
428     final String prefix = "Python ";
429     if (versionString != null && versionString.startsWith(prefix)) {
430       final String version = versionString.substring(prefix.length());
431       int dot = version.indexOf('.');
432       if (dot > 0) {
433         dot = version.indexOf('.', dot + 1);
434         if (dot > 0) {
435           return version.substring(0, dot);
436         }
437         return version;
438       }
439     }
440     return null;
441   }
442
443   @Override
444   public String fetchExternalDocumentation(Project project, PsiElement element, List<String> docUrls) {
445     return null;
446   }
447
448   @Override
449   public boolean hasDocumentationFor(PsiElement element, PsiElement originalElement) {
450     return getUrlFor(element, originalElement, false) != null;
451   }
452
453   @Override
454   public boolean canPromptToConfigureDocumentation(@NotNull PsiElement element) {
455     final PsiFile containingFile = element.getContainingFile();
456     if (containingFile instanceof PyFile) {
457       final Project project = element.getProject();
458       final VirtualFile vFile = containingFile.getVirtualFile();
459       if (vFile != null && ProjectRootManager.getInstance(project).getFileIndex().isInLibraryClasses(vFile)) {
460         final QualifiedName qName = QualifiedNameFinder.findCanonicalImportPath(element, element);
461         if (qName != null && qName.getComponentCount() > 0) {
462           return true;
463         }
464       }
465     }
466     return false;
467   }
468
469   @Override
470   public void promptToConfigureDocumentation(@NotNull PsiElement element) {
471     final Project project = element.getProject();
472     final QualifiedName qName = QualifiedNameFinder.findCanonicalImportPath(element, element);
473     if (qName != null && qName.getComponentCount() > 0) {
474       ApplicationManager.getApplication().invokeLater(() -> {
475         final int rc = Messages.showOkCancelDialog(project,
476                                                    "No external documentation URL configured for module " + qName.getComponents().get(0) +
477                                                    ".\nWould you like to configure it now?",
478                                                    "Python External Documentation",
479                                                    Messages.getQuestionIcon());
480         if (rc == Messages.OK) {
481           ShowSettingsUtilImpl.showSettingsDialog(project, PythonDocumentationConfigurable.ID, "");
482         }
483       }, ModalityState.NON_MODAL);
484     }
485   }
486
487   @Nullable
488   @Override
489   public PsiElement getCustomDocumentationElement(@NotNull Editor editor,
490                                                   @NotNull PsiFile file,
491                                                   @Nullable PsiElement contextElement) {
492     if (contextElement != null &&
493         PythonDialectsTokenSetProvider.INSTANCE.getKeywordTokens().contains(contextElement.getNode().getElementType())) {
494       return contextElement;
495     }
496     return super.getCustomDocumentationElement(editor, file, contextElement);
497   }
498
499   @Nullable
500   private static PyClass inferContainingClassOf(PsiElement context) {
501     if (context instanceof PyClass) return (PyClass)context;
502     if (context instanceof PyFunction) {
503       return ((PyFunction)context).getContainingClass();
504     }
505     else {
506       return PsiTreeUtil.getParentOfType(context, PyClass.class);
507     }
508   }
509
510   @Nullable
511   private static PyClass inferClassOfParameter(@NotNull PsiElement context) {
512     if (context instanceof PyNamedParameter) {
513       final PyType type = TypeEvalContext.userInitiated(context.getProject(), context.getContainingFile()).getType(
514         (PyNamedParameter)context);
515       if (type instanceof PyClassType) {
516         return ((PyClassType)type).getPyClass();
517       }
518     }
519     return null;
520   }
521
522   public static final LinkWrapper LinkMyClass = new LinkWrapper(LINK_TYPE_CLASS);
523   // link item to containing class
524 }