PY-16987 Google and Numpy docstrings return null as parameter type if it wasn't speci...
[idea/community.git] / python / src / com / jetbrains / python / documentation / docstrings / DocStringUtil.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.intellij.lang.ASTNode;
19 import com.intellij.openapi.module.Module;
20 import com.intellij.openapi.module.ModuleManager;
21 import com.intellij.openapi.module.ModuleUtilCore;
22 import com.intellij.openapi.ui.Messages;
23 import com.intellij.openapi.util.TextRange;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.psi.PsiComment;
26 import com.intellij.psi.PsiElement;
27 import com.intellij.psi.PsiFile;
28 import com.intellij.psi.util.PsiTreeUtil;
29 import com.intellij.util.ArrayUtil;
30 import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
31 import com.jetbrains.python.documentation.PyDocumentationSettings;
32 import com.jetbrains.python.psi.*;
33 import com.jetbrains.python.psi.impl.PyPsiUtils;
34 import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl;
35 import com.jetbrains.python.toolbox.Substring;
36 import org.jetbrains.annotations.NonNls;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.util.List;
41
42 /**
43  * User: catherine
44  */
45 public class DocStringUtil {
46   private DocStringUtil() {
47   }
48
49   @Nullable
50   public static String getDocStringValue(@NotNull PyDocStringOwner owner) {
51     return PyPsiUtils.strValue(owner.getDocStringExpression());
52   }
53
54   /**
55    * Attempts to detect docstring format from given text and parses it into corresponding structured docstring.
56    * It's recommended to use more reliable {@link #parse(String, PsiElement)} that fallbacks to format specified in settings.
57    *
58    * @return structured docstring for one of supported formats or instance of {@link PlainDocString} if none was recognized.
59    * @see #parse(String, PsiElement) 
60    */
61   @NotNull
62   public static StructuredDocString parse(@NotNull String text) {
63     return parse(text, null);
64   }
65
66   /**
67    * Attempts to detects docstring format first from given text, next from settings and parses text into corresponding structured docstring.
68    *
69    * @return structured docstring for one of supported formats or instance of {@link PlainDocString} if none was recognized.
70    * @see DocStringFormat#ALL_NAMES_BUT_PLAIN
71    * @see #guessDocStringFormat(String, PsiElement)
72    */
73   @NotNull
74   public static StructuredDocString parse(@NotNull String text, @Nullable PsiElement anchor) {
75     final DocStringFormat format = guessDocStringFormat(text, anchor);
76     return parseDocStringContent(format, text);
77   }
78   
79   @NotNull
80   public static StructuredDocString parseDocString(@NotNull DocStringFormat format,
81                                                    @NotNull PyStringLiteralExpression literalExpression) {
82     return parseDocString(format, literalExpression.getStringNodes().get(0));
83   }
84
85   @NotNull
86   public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull ASTNode node) {
87     //Preconditions.checkArgument(node.getElementType() == PyTokenTypes.DOCSTRING);
88     return parseDocString(format, node.getText());
89   }
90
91
92   @NotNull
93   public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull String stringText) {
94     return parseDocString(format, stripSuffixAndQuotes(stringText));
95   }
96
97   @NotNull
98   public static StructuredDocString parseDocStringContent(@NotNull DocStringFormat format, @NotNull String stringContent) {
99     return parseDocString(format, new Substring(stringContent));
100   }
101
102   @NotNull
103   public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull Substring content) {
104     switch (format) {
105       case REST:
106         return new SphinxDocString(content);
107       case EPYTEXT:
108         return new EpydocString(content);
109       case GOOGLE:
110         return new GoogleCodeStyleDocString(content);
111       case NUMPY:
112         return new NumpyDocString(content);
113       default:
114         return new PlainDocString(content);
115     }
116   }
117
118   @NotNull
119   private static Substring stripSuffixAndQuotes(@NotNull String text) {
120     final TextRange contentRange = PyStringLiteralExpressionImpl.getNodeTextRange(text);
121     return new Substring(text, contentRange.getStartOffset(), contentRange.getEndOffset());
122   }
123   
124   /**
125    * @return docstring format inferred heuristically solely from its content. For more reliable result use anchored version 
126    * {@link #guessDocStringFormat(String, PsiElement)} of this method.
127    * @see #guessDocStringFormat(String, PsiElement) 
128    */
129   @NotNull
130   public static DocStringFormat guessDocStringFormat(@NotNull String text) {
131     if (isLikeNumpyDocstring(text)) {
132       return DocStringFormat.NUMPY;
133     }
134     if (isLikeGoogleDocString(text)) {
135       return DocStringFormat.GOOGLE;
136     }
137     if (isLikeEpydocDocString(text)) {
138       return DocStringFormat.EPYTEXT;
139     }
140     if (isLikeSphinxDocString(text)) {
141       return DocStringFormat.REST;
142     }
143     return DocStringFormat.PLAIN;
144   }
145
146   /**
147    * @return docstring inferred heuristically and if unsuccessful fallback to configured format retrieved from anchor PSI element 
148    * @see #getConfiguredDocStringFormat(PsiElement) 
149    */
150   @NotNull
151   public static DocStringFormat guessDocStringFormat(@NotNull String text, @Nullable PsiElement anchor) {
152     final DocStringFormat guessed = guessDocStringFormat(text);
153     return guessed == DocStringFormat.PLAIN && anchor != null ? getConfiguredDocStringFormat(anchor) : guessed;
154   }
155
156   /**
157    * @return docstring format configured for file or module containing given anchor PSI element
158    * @see PyDocumentationSettings#getFormatForFile(PsiFile)
159    */
160   @NotNull
161   public static DocStringFormat getConfiguredDocStringFormat(@NotNull PsiElement anchor) {
162     final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(getModuleForElement(anchor));
163     return settings.getFormatForFile(anchor.getContainingFile());
164   }
165
166   public static boolean isLikeSphinxDocString(@NotNull String text) {
167     return text.contains(":param ") || 
168            text.contains(":return:") || text.contains(":returns:") || 
169            text.contains(":rtype") || text.contains(":type");
170   }
171
172   public static boolean isLikeEpydocDocString(@NotNull String text) {
173     return text.contains("@param ") || text.contains("@return:") || text.contains("@rtype") || text.contains("@type");
174   }
175
176   public static boolean isLikeGoogleDocString(@NotNull String text) {
177     for (@NonNls String title : StringUtil.findMatches(text, GoogleCodeStyleDocString.SECTION_HEADER, 1)) {
178       if (SectionBasedDocString.isValidSectionTitle(title)) {
179         return true;
180       }
181     }
182     return false;
183   }
184
185   public static boolean isLikeNumpyDocstring(@NotNull String text) {
186     final String[] lines = StringUtil.splitByLines(text, false);
187     for (int i = 0; i < lines.length; i++) {
188       final String line = lines[i];
189       if (NumpyDocString.SECTION_HEADER.matcher(line).matches() && i > 0) {
190         @NonNls final String lineBefore = lines[i - 1];
191         if (SectionBasedDocString.SECTION_NAMES.contains(lineBefore.trim().toLowerCase())) {
192           return true;
193         }
194       }
195     }
196     return false;
197   }
198
199   /**
200    * Looks for a doc string under given parent.
201    *
202    * @param parent where to look. For classes and functions, this would be PyStatementList, for modules, PyFile.
203    * @return the defining expression, or null.
204    */
205   @Nullable
206   public static PyStringLiteralExpression findDocStringExpression(@Nullable PyElement parent) {
207     if (parent != null) {
208       PsiElement seeker = PyPsiUtils.getNextNonCommentSibling(parent.getFirstChild(), false);
209       if (seeker instanceof PyExpressionStatement) seeker = PyPsiUtils.getNextNonCommentSibling(seeker.getFirstChild(), false);
210       if (seeker instanceof PyStringLiteralExpression) return (PyStringLiteralExpression)seeker;
211     }
212     return null;
213   }
214
215   @Nullable
216   public static StructuredDocString getStructuredDocString(@NotNull PyDocStringOwner owner) {
217     final String value = owner.getDocStringValue();
218     return value == null ? null : parse(value, owner);
219   }
220
221   /**
222    * Returns containing docstring expression of class definition, function definition or module. 
223    * Useful to test whether particular PSI element is or belongs to such docstring.
224    */
225   @Nullable
226   public static PyStringLiteralExpression getParentDefinitionDocString(@NotNull PsiElement element) {
227     final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(element, PyDocStringOwner.class);
228     if (docStringOwner != null) {
229       final PyStringLiteralExpression docString = docStringOwner.getDocStringExpression();
230       if (PsiTreeUtil.isAncestor(docString, element, false)) {
231         return docString;
232       }
233     }
234     return null;
235   }
236
237   public static boolean isDocStringExpression(@NotNull PyExpression expression) {
238     if (getParentDefinitionDocString(expression) == expression) {
239       return true;
240     }
241     if (expression instanceof PyStringLiteralExpression) {
242       return isVariableDocString((PyStringLiteralExpression)expression);
243     }
244     return false;
245   }
246
247   @Nullable
248   public static String getAttributeDocComment(@NotNull PyTargetExpression attr) {
249     if (attr.getParent() instanceof PyAssignmentStatement) {
250       final PyAssignmentStatement assignment = (PyAssignmentStatement)attr.getParent();
251       final PsiElement prevSibling = PyPsiUtils.getPrevNonWhitespaceSibling(assignment);
252       if (prevSibling instanceof PsiComment && prevSibling.getText().startsWith("#:")) {
253         return prevSibling.getText().substring(2);
254       }
255     }
256     return null;
257   }
258
259   public static boolean isVariableDocString(@NotNull PyStringLiteralExpression expr) {
260     final PsiElement parent = expr.getParent();
261     if (!(parent instanceof PyExpressionStatement)) {
262       return false;
263     }
264     final PsiElement prevElement = PyPsiUtils.getPrevNonCommentSibling(parent, true);
265     if (prevElement instanceof PyAssignmentStatement) {
266       if (expr.getText().contains("type:")) return true;
267
268       final PyAssignmentStatement assignmentStatement = (PyAssignmentStatement)prevElement;
269       final ScopeOwner scope = PsiTreeUtil.getParentOfType(prevElement, ScopeOwner.class);
270       if (scope instanceof PyClass || scope instanceof PyFile) {
271         return true;
272       }
273       if (scope instanceof PyFunction) {
274         for (PyExpression target : assignmentStatement.getTargets()) {
275           if (PyUtil.isInstanceAttribute(target)) {
276             return true;
277           }
278         }
279       }
280     }
281     return false;
282   }
283
284   /**
285    * Checks that docstring format is set either via element module's {@link com.jetbrains.python.PyNames.DOCFORMAT} attribute or
286    * in module settings. If none of them applies, show standard choose dialog, asking user to pick one and updates module settings
287    * accordingly.
288    *
289    * @param anchor PSI element that will be used to locate containing file and project module
290    * @return false if no structured docstring format was specified initially and user didn't select any, true otherwise
291    */
292   public static boolean ensureNotPlainDocstringFormat(@NotNull PsiElement anchor) {
293     return ensureNotPlainDocstringFormatForFile(anchor.getContainingFile(), getModuleForElement(anchor));
294   }
295
296   @NotNull
297   private static Module getModuleForElement(@NotNull PsiElement element) {
298     Module module = ModuleUtilCore.findModuleForPsiElement(element);
299     if (module == null) {
300       module = ModuleManager.getInstance(element.getProject()).getModules()[0];
301     }
302     return module;
303   }
304
305   private static boolean ensureNotPlainDocstringFormatForFile(@NotNull PsiFile file, @NotNull Module module) {
306     final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(module);
307     if (settings.isPlain(file)) {
308       final List<String> values = DocStringFormat.ALL_NAMES_BUT_PLAIN;
309       final int i =
310         Messages.showChooseDialog("Docstring format:", "Select Docstring Type", ArrayUtil.toStringArray(values), values.get(0), null);
311       if (i < 0) {
312         return false;
313       }
314       settings.setFormat(DocStringFormat.fromNameOrPlain(values.get(i)));
315     }
316     return true;
317   }
318 }