a24d45d9ee2176fe54a839b0864915ca52397ca8
[idea/community.git] / python / src / com / jetbrains / python / documentation / docstrings / PyStructuredDocstringFormatter.java
1 /*
2  * Copyright 2000-2014 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.docstrings;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.execution.configurations.GeneralCommandLine;
20 import com.intellij.execution.process.ProcessOutput;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.module.Module;
23 import com.intellij.openapi.module.ModuleManager;
24 import com.intellij.openapi.module.ModuleUtilCore;
25 import com.intellij.openapi.projectRoots.Sdk;
26 import com.intellij.openapi.vfs.CharsetToolkit;
27 import com.intellij.psi.PsiElement;
28 import com.jetbrains.python.HelperPackage;
29 import com.jetbrains.python.PyBundle;
30 import com.jetbrains.python.PythonHelper;
31 import com.jetbrains.python.psi.PyIndentUtil;
32 import com.jetbrains.python.psi.PyStringLiteralExpression;
33 import com.jetbrains.python.psi.StructuredDocString;
34 import com.jetbrains.python.sdk.PySdkUtil;
35 import com.jetbrains.python.sdk.PythonSdkType;
36 import com.jetbrains.python.toolbox.Substring;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.io.File;
41 import java.nio.ByteBuffer;
42 import java.nio.charset.Charset;
43 import java.util.ArrayList;
44 import java.util.List;
45
46 /**
47  * @author yole
48  */
49 public class PyStructuredDocstringFormatter {
50   private static final Logger LOG = Logger.getInstance(PyStructuredDocstringFormatter.class);
51   private static final Charset DEFAULT_CHARSET = CharsetToolkit.UTF8_CHARSET;
52
53   private PyStructuredDocstringFormatter() {
54   }
55
56   /**
57    * @param docstring docstring text without string literal prefix, without quotes and already escaped. 
58    *                  Supposedly result of {@link PyStringLiteralExpression#getStringValue()}.
59    */
60   @Nullable
61   public static List<String> formatDocstring(@NotNull final PsiElement element, @NotNull final String docstring) {
62     Module module = ModuleUtilCore.findModuleForPsiElement(element);
63     if (module == null) {
64       final Module[] modules = ModuleManager.getInstance(element.getProject()).getModules();
65       if (modules.length == 0) return Lists.newArrayList();
66       module = modules[0];
67     }
68     if (module == null) return Lists.newArrayList();
69     final List<String> result = new ArrayList<>();
70
71     final String preparedDocstring = PyIndentUtil.removeCommonIndent(docstring, true).trim();
72
73     final HelperPackage formatter;
74     final StructuredDocString structuredDocString;
75     final DocStringFormat format = DocStringUtil.guessDocStringFormat(preparedDocstring, element);
76     if (format == DocStringFormat.GOOGLE) {
77       formatter = PythonHelper.GOOGLE_FORMATTER;
78       structuredDocString = DocStringUtil.parseDocStringContent(DocStringFormat.GOOGLE, preparedDocstring);
79     }
80     else if (format == DocStringFormat.NUMPY) {
81       formatter = PythonHelper.NUMPY_FORMATTER;
82       structuredDocString = DocStringUtil.parseDocStringContent(DocStringFormat.NUMPY, preparedDocstring);
83     }
84     else if (format == DocStringFormat.EPYTEXT) {
85       formatter = PythonHelper.EPYDOC_FORMATTER;
86       structuredDocString = DocStringUtil.parseDocStringContent(DocStringFormat.EPYTEXT, preparedDocstring);
87       result.add(formatStructuredDocString(structuredDocString));
88     }
89     else if (format == DocStringFormat.REST) {
90       formatter = PythonHelper.REST_FORMATTER;
91       structuredDocString = DocStringUtil.parseDocStringContent(DocStringFormat.REST, preparedDocstring);
92     }
93
94     else {
95       return null;
96     }
97
98     final String output = runExternalTool(module, formatter, preparedDocstring);
99     if (output != null) {
100       result.add(0, output);
101     }
102     else {
103       result.add(0, structuredDocString.getDescription());
104     }
105
106     return result;
107   }
108
109   @Nullable
110   private static String runExternalTool(@NotNull final Module module,
111                                         @NotNull final HelperPackage formatter,
112                                         @NotNull final String docstring) {
113     final Sdk sdk;
114     final String missingInterpreterMessage;
115     if (formatter == PythonHelper.EPYDOC_FORMATTER) {
116       sdk = PythonSdkType.findPython2Sdk(module);
117       missingInterpreterMessage = PyBundle.message("QDOC.epydoc.python2.sdk.not.found");
118     }
119     else {
120       sdk = PythonSdkType.findLocalCPython(module);
121       missingInterpreterMessage = PyBundle.message("QDOC.sdk.not.found");
122     }
123     if (sdk == null) {
124       LOG.warn("Python SDK for docstring formatter " + formatter +  " is not found");
125       return "<p color=\"red\">" + missingInterpreterMessage + "</p>";
126     }
127
128     final String sdkHome = sdk.getHomePath();
129     if (sdkHome == null) return null;
130
131     final ByteBuffer encoded = DEFAULT_CHARSET.encode(docstring);
132     final byte[] data = new byte[encoded.limit()];
133     encoded.get(data);
134
135     final GeneralCommandLine commandLine = formatter.newCommandLine(sdk, Lists.newArrayList());
136     commandLine.setCharset(DEFAULT_CHARSET);
137     
138     LOG.debug("Command for launching docstring formatter: " + commandLine.getCommandLineString());
139     
140     final ProcessOutput output = PySdkUtil.getProcessOutput(commandLine, new File(sdkHome).getParent(), null, 5000, data, false);
141     if (!output.checkSuccess(LOG)) {
142       LOG.info("Malformed docstring:\n" + docstring);
143       return null;
144     }
145     return output.getStdout();
146   }
147
148   private static String formatStructuredDocString(@NotNull final StructuredDocString docString) {
149     final StringBuilder result = new StringBuilder();
150
151     final String attributeDescription = docString.getAttributeDescription();
152     if (attributeDescription != null) {
153       result.append(attributeDescription);
154       final String attrType = docString.getParamType(null);
155       if (attrType != null) {
156         result.append(" <i>Type: ").append(attrType).append("</i>");
157       }
158     }
159
160     formatParameterDescriptions(docString, result, false);
161     formatParameterDescriptions(docString, result, true);
162
163     final String returnDescription = docString.getReturnDescription();
164     final String returnType = docString.getReturnType();
165     if (returnDescription != null || returnType != null) {
166       result.append("<br><b>Return value:</b><br>");
167       if (returnDescription != null) {
168         result.append(returnDescription);
169       }
170       if (returnType != null) {
171         result.append(" <i>Type: ").append(returnType).append("</i>");
172       }
173     }
174
175     final List<String> raisedException = docString.getRaisedExceptions();
176     if (raisedException.size() > 0) {
177       result.append("<br><b>Raises:</b><br>");
178       for (String s : raisedException) {
179         result.append("<b>").append(s).append("</b> - ").append(docString.getRaisedExceptionDescription(s)).append("<br>");
180       }
181     }
182
183     if (docString instanceof TagBasedDocString) {
184       final TagBasedDocString taggedDocString = (TagBasedDocString)docString;
185       final List<String> additionalTags = taggedDocString.getAdditionalTags();
186       if (!additionalTags.isEmpty()) {
187         result.append("<br/><br/><b>Additional:</b><br/>");
188         result.append("<table>");
189         for (String tagName : additionalTags) {
190           final List<Substring> args = taggedDocString.getTagArguments(tagName);
191           for (Substring arg : args) {
192             final String s = arg.toString();
193             result.append("<tr><td align=\"right\"><b>").append(tagName);
194             result.append(" ").append(s).append(":</b>");
195             result.append("</td><td>").append(taggedDocString.getTagValue(tagName, s)).append("</td></tr>");
196           }
197           result.append("</table>");
198         }
199       }
200     }
201     return result.toString();
202   }
203
204   private static void formatParameterDescriptions(@NotNull final StructuredDocString docString,
205                                                   @NotNull final StringBuilder result,
206                                                   boolean keyword) {
207     final List<String> parameters = keyword ? docString.getKeywordArguments() : docString.getParameters();
208     if (parameters.size() > 0) {
209       result.append("<br><b>").append(keyword ? "Keyword arguments:" : "Parameters").append("</b><br>");
210       for (String parameter : parameters) {
211         final String description = keyword ? docString.getKeywordArgumentDescription(parameter) : docString.getParamDescription(parameter);
212         result.append("<b>");
213         result.append(parameter);
214         result.append("</b>: ");
215         if (description != null) {
216           result.append(description);
217         }
218         final String paramType = docString.getParamType(parameter);
219         if (paramType != null) {
220           result.append(" <i>Type: ").append(paramType).append("</i>");
221         }
222         result.append("<br>");
223       }
224     }
225   }
226 }