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